paymeuz-clickuz-master

Use this skill when integrating Payme.uz or Click.uz payment gateways into NestJS applications. Covers transaction flows, webhook handling, signature verification, TypeScript types, and error codes.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "paymeuz-clickuz-master" with this command: npx skills add idealprojectuz/paymeuz-clickuz-master-skills/idealprojectuz-paymeuz-clickuz-master-skills-paymeuz-clickuz-master

You are an expert Uzbek payment gateway integration specialist with deep production experience in Payme.uz and Click.uz systems inside NestJS applications.

Before writing any code: read the existing project structure to understand the entity model (User, Order, Product, etc.) and ID types (number, string, bigint). Adapt all patterns to the actual project — do not assume a fixed schema.


Recommended File Structure

src/
├── click/
│   ├── click.module.ts
│   ├── click.service.ts
│   ├── click.controller.ts          # optional, can use shared payment.controller
│   ├── constants/
│   │   ├── click-error.ts
│   │   └── click-action.ts
│   ├── dto/
│   │   └── click-request.dto.ts
│   └── interfaces/
│       └── md5-params.interface.ts
├── payme/
│   ├── payme.module.ts
│   ├── payme.service.ts
│   ├── constants/
│   │   ├── payme-error.ts
│   │   ├── transaction-state.ts
│   │   ├── transaction-methods.ts
│   │   └── canceling-reasons.ts
│   ├── dto/                         # one DTO per RPC method
│   └── types/
│       └── request-body.type.ts
├── payment/
│   ├── payment.module.ts
│   ├── payment.controller.ts        # shared webhook controller
│   └── payment.service.ts           # payment link generator
└── auth/guards/
    └── payme.guard.ts

NestJS Module Setup

// src/payme/payme.module.ts
@Module({
  providers: [PaymeService],
  exports: [PaymeService],
})
export class PaymeModule {}

// src/click/click.module.ts
@Module({
  providers: [ClickService],
  exports: [ClickService],
})
export class ClickModule {}

// src/payment/payment.module.ts
@Module({
  imports: [PaymeModule, ClickModule],
  controllers: [PaymentController],
})
export class PaymentModule {}

// src/app.module.ts — PaymentModule ni import qiling
@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true }), PaymentModule],
})
export class AppModule {}

Environment Variables

PAYME_MERCHANT_ID=your_merchant_id
PAYME_LOGIN=Paycom              # "Paycom" in test mode, merchant_id in prod
PAYME_PASSWORD=your_secret_key
CLICK_SERVICE_ID=12345
CLICK_MERCHANT_ID=67890
CLICK_SECRET=your_click_secret_key

Payment Link Generation

// Payme — amount tiynda (1 UZS = 100 tiyn)
// ac.<key> — loyihadagi account identifikatori (user_id, order_id, etc.)
const data = `m=${PAYME_MERCHANT_ID};l=uz;ac.user_id=${entityId};a=${amount * 100};c=https://redirect.url`;
const encoded = Buffer.from(data).toString("base64");
const paymeLink = `https://checkout.paycom.uz/${encoded}`;

// Click — amount UZSda, transaction_param = entityId
const clickLink =
  `https://my.click.uz/services/pay` +
  `?service_id=${CLICK_SERVICE_ID}` +
  `&merchant_id=${CLICK_MERCHANT_ID}` +
  `&amount=${amount}` +
  `&transaction_param=${entityId}` +
  `&return_url=https://redirect.url`;

Click.uz Integration

Flow (2-phase)

User pays → Click calls your server twice:
  Phase 1 (action=0): PREPARE  — validate, reserve
  Phase 2 (action=1): COMPLETE — finalize, update DB

Error Codes

// src/click/constants/click-error.ts
export const ClickError = {
  Success: 0,
  SignFailed: -1, // MD5 hash mismatch
  InvalidAmount: -2, // amount mismatch
  ActionNotFound: -3, // action not 0 or 1
  AlreadyPaid: -4, // transaction already paid
  UserNotFound: -5, // entity not found in your DB
  TransactionNotFound: -6, // merchant_prepare_id not found
  FailedToUpdateUser: -7, // DB update error
  BadRequest: -8, // missing/invalid params
  TransactionCanceled: -9, // already cancelled
};

Signature Verification (MD5)

import { createHash } from "node:crypto";

// Prepare (action=0): merchantPrepareId YO'Q
const prepareContent = `${clickTransId}${serviceId}${secretKey}${merchantTransId}${amount}${action}${signTime}`;

// Complete (action=1): merchantPrepareId QO'SHILADI
const completeContent = `${clickTransId}${serviceId}${secretKey}${merchantTransId}${merchantPrepareId}${amount}${action}${signTime}`;

const hash = createHash("md5").update(content).digest("hex");
if (hash !== sign_string)
  return { error: ClickError.SignFailed, error_note: "Invalid sign_string" };

DTO Types

// src/click/dto/click-request.dto.ts
import {
  IsIn,
  IsNotEmpty,
  IsNumber,
  IsOptional,
  IsString,
} from "class-validator";
import { Type } from "class-transformer";

export class ClickRequestDto {
  @IsNumber()
  @Type(() => Number)
  click_trans_id: number;

  @IsNumber()
  @Type(() => Number)
  service_id: number;

  @IsNumber()
  @Type(() => Number)
  click_paydoc_id: number;

  @IsOptional()
  @IsString()
  merchant_user_id?: string;

  @IsString()
  @IsNotEmpty()
  merchant_trans_id: string; // your entityId (user, order, etc.)

  @IsOptional()
  @IsString()
  param2?: string;

  @Type(() => Number)
  amount: number; // UZS — always parseFloat() it

  @IsIn([0, 1])
  @Type(() => Number)
  action: 0 | 1;

  @IsNumber()
  @Type(() => Number)
  error: number; // >0 means Click-side failure in complete

  @IsString()
  error_note: string;

  @IsString()
  sign_time: string;

  @IsString()
  sign_string: string;

  @IsNumber()
  @Type(() => Number)
  merchant_prepare_id: number; // your server generates in prepare; sent back in complete
}

// src/click/interfaces/md5-params.interface.ts
export interface ClickMd5Params {
  clickTransId: string;
  serviceId: number;
  secretKey: string;
  merchantTransId: string;
  merchantPrepareId?: number; // undefined in prepare, required in complete
  amount: number;
  action: number;
  signTime: string;
}

Click Service Pattern

@Injectable()
export class ClickService {
  private readonly logger = new Logger(ClickService.name);
  private readonly secretKey: string;

  constructor(private readonly config: ConfigService) {
    this.secretKey = this.config.getOrThrow<string>("CLICK_SECRET");
  }

  async handleWebhook(body: ClickRequestDto) {
    this.logger.log(
      `Click webhook: action=${body.action} trans=${body.click_trans_id}`,
    );
    body.amount = parseFloat(body.amount as any); // always normalize

    switch (+body.action) {
      case 0:
        return this.prepare(body);
      case 1:
        return this.complete(body);
      default:
        return {
          error: ClickError.ActionNotFound,
          error_note: "Invalid action",
        };
    }
  }

  async prepare(body: ClickRequestDto) {
    // 1. Verify signature (no merchantPrepareId)
    if (!this.verifySign(body))
      return {
        error: ClickError.SignFailed,
        error_note: "Invalid sign_string",
      };

    // 2. Find entity — adapt to your model (user, order, product…)
    const entity = await this.findEntity(body.merchant_trans_id);
    if (!entity)
      return { error: ClickError.UserNotFound, error_note: "Entity not found" };

    // 3. Check idempotency
    const existing = await this.findTransaction(body.click_trans_id.toString());
    if (existing?.status === "PAID")
      return { error: ClickError.AlreadyPaid, error_note: "Already paid" };
    if (existing?.status === "CANCELED")
      return { error: ClickError.TransactionCanceled, error_note: "Cancelled" };

    // 4. Create PENDING record
    const prepareId = Date.now();
    await this.createTransaction({
      transactionId: body.click_trans_id.toString(),
      prepareId,
      amount: body.amount,
      entityId: body.merchant_trans_id,
      status: "PENDING",
      provider: "CLICK",
    });

    return {
      click_trans_id: body.click_trans_id,
      merchant_trans_id: body.merchant_trans_id,
      merchant_prepare_id: prepareId,
      error: ClickError.Success,
      error_note: "Success",
    };
  }

  async complete(body: ClickRequestDto) {
    // 1. Verify signature (with merchantPrepareId)
    if (!this.verifySign(body))
      return {
        error: ClickError.SignFailed,
        error_note: "Invalid sign_string",
      };

    // 2. Find transaction
    const tx = await this.findTransactionByPrepare(
      body.click_trans_id.toString(),
      body.merchant_prepare_id,
    );
    if (!tx)
      return { error: ClickError.TransactionNotFound, error_note: "Not found" };

    if (tx.status === "PAID")
      return { error: ClickError.AlreadyPaid, error_note: "Already paid" };
    if (tx.status === "CANCELED")
      return { error: ClickError.TransactionCanceled, error_note: "Cancelled" };

    if (Number(body.amount) !== Number(tx.amount))
      return { error: ClickError.InvalidAmount, error_note: "Amount mismatch" };

    // 3. Click error > 0 = Click-side failure → cancel
    if (body.error > 0) {
      await this.cancelTransaction(tx.id);
      return { error: body.error, error_note: "Failed" };
    }

    // 4. Mark PAID + business logic
    await this.markPaid(tx.id);
    await this.onPaymentSuccess(tx.id); // inject your own success handler

    return {
      click_trans_id: body.click_trans_id,
      merchant_trans_id: body.merchant_trans_id,
      merchant_confirm_id: null,
      error: ClickError.Success,
      error_note: "Success",
    };
  }

  private verifySign(body: ClickRequestDto): boolean {
    const params: ClickMd5Params = {
      clickTransId: body.click_trans_id.toString(),
      serviceId: body.service_id,
      secretKey: this.secretKey,
      merchantTransId: body.merchant_trans_id,
      amount: body.amount,
      action: body.action,
      signTime: body.sign_time,
      // only in complete:
      ...(body.action === 1 && { merchantPrepareId: body.merchant_prepare_id }),
    };
    const content = [
      params.clickTransId,
      params.serviceId,
      params.secretKey,
      params.merchantTransId,
      params.merchantPrepareId ?? "",
      params.amount,
      params.action,
      params.signTime,
    ].join("");
    return createHash("md5").update(content).digest("hex") === body.sign_string;
  }
}

GetInfo (Optional — Advanced Shop)

GetInfo — Click tizimi to'lov oynasida foydalanuvchiga kerakli ma'lumotni ko'rsatish uchun ixtiyoriy so'rov. Barcha loyihalarda kerak emas.

Click so'rovi:

{
  "action": 0,
  "service_id": 123,
  "params": {
    "contract": "***",
    "full_name": "***",
    "service_type": "***"
  }
}

Muvaffaqiyatli javob:

{
  "error": 0,
  "error_note": "Muvaffaqiyatli",
  "params": {
    "caller_id": "...",
    "full_name": "...",
    "address": "...",
    "balance": "...",
    "account": "..."
  }
}

Muvaffaqiyatsiz javob:

{
  "error": -5,
  "error_note": "Abonent topilmadi"
}

Tekshirish shartlari:

  • action must be 0
  • service_id must match CLICK_SERVICE_ID
  • params keys depend on your merchant agreement with Click
// Faqat kerak bo'lsa implement qiling
async getInfo(body: { action: number; service_id: number; params: Record<string, any> }) {
  if (body.service_id !== Number(process.env.CLICK_SERVICE_ID) || body.action !== 0)
    return { error: -8, error_note: 'Service unavailable' };

  // Loyihaga qarab entity toping (user, contract, order…)
  const entity = await this.findEntityByParams(body.params);
  if (!entity) return { error: -5, error_note: 'Entity not found' };

  // Kerakli fieldlarni qaytaring — Click merchant agreement ga bog'liq
  return {
    error: 0,
    error_note: 'Muvaffaqiyatli',
    params: {
      caller_id: entity.id.toString(),
      full_name: entity.name?.slice(0, 20) ?? '',
      // qo'shimcha fieldlar: balance, address, account, etc.
    },
  };
}

Click GetInfo params reference:

KeyDescription
caller_idEntity ID
full_nameFIO (max 20 char)
accountLitsenziya/hisob raqami
addressManzil
balanceBalans
contractShartnoma raqami
phone_numTelefon
emailEmail
periodDavr
amountSumma
service_typeXizmat turi

Payme.uz Integration

Flow (JSON-RPC 2.0)

Payme sends POST requests → your single endpoint
Auth: Basic base64("Paycom:SECRET_KEY") → always HTTP 200!

Method sequence:
  1. CheckPerformTransaction — can we charge? validate amount + account
  2. CreateTransaction       — create pending record in your DB
  3. PerformTransaction      — mark paid, run business logic
  4. CancelTransaction       — cancel (before or after perform)
  5. CheckTransaction        — get transaction status
  6. GetStatement            — list transactions in time range

Transaction States

// src/payme/constants/transaction-state.ts
export const TransactionState = {
  Pending: 1, // yaratildi, kutmoqda
  Paid: 2, // muvaffaqiyatli bajarildi
  PendingCanceled: -1, // Perform dan oldin bekor qilindi
  PaidCanceled: -2, // Perform dan keyin bekor qilindi (refund)
};

Transaction Methods

// src/payme/constants/transaction-methods.ts
export enum TransactionMethods {
  CheckPerformTransaction = "CheckPerformTransaction",
  CreateTransaction = "CreateTransaction",
  CheckTransaction = "CheckTransaction",
  PerformTransaction = "PerformTransaction",
  CancelTransaction = "CancelTransaction",
  GetStatement = "GetStatement",
}

Canceling Reasons

// src/payme/constants/canceling-reasons.ts
export const CancelingReasons = {
  RecipientNotFound: 1,
  ErrorWhilePerformingDebitOperation: 2,
  TransactionFailed: 3,
  CanceledDueToTimeout: 4,
  Refund: 5,
  UnknownError: 10,
};

Error Objects

// src/payme/constants/payme-error.ts
export const PaymeError = {
  InvalidAmount: {
    code: -31001,
    message: {
      uz: "Noto'g'ri summa",
      ru: "Недопустимая сумма",
      en: "Invalid amount",
    },
  },
  TransactionNotFound: {
    code: -31003,
    message: {
      uz: "Tranzaksiya topilmadi",
      ru: "Транзакция не найдена",
      en: "Transaction not found",
    },
  },
  CantDoOperation: {
    code: -31008,
    message: {
      uz: "Operatsiyani bajarib bo'lmaydi",
      ru: "Невозможно выполнить операцию",
      en: "Can't perform operation",
    },
  },
  AccountNotFound: {
    code: -31050,
    message: {
      uz: "Hisob topilmadi",
      ru: "Аккаунт не найден",
      en: "Account not found",
    },
  },
  AlreadyDone: {
    code: -31060,
    message: {
      uz: "To'lov amalga oshirilgan",
      ru: "Оплата выполнена",
      en: "Already paid",
    },
  },
  InvalidAuthorization: {
    code: -32504,
    message: {
      uz: "Avtorizatsiya xatosi",
      ru: "Ошибка авторизации",
      en: "Authorization error",
    },
  },
};

DTO Types

// Barcha DTOlar uchun umumiy baza
interface PaymeRequestBase {
  id: number; // JSON-RPC id — javobda qaytarilishi shart
  method: TransactionMethods;
}

export interface CheckPerformTransactionDto extends PaymeRequestBase {
  params: {
    amount: number; // tiyn (1 UZS = 100 tiyn)
    account: Record<string, any>; // loyihaga qarab: { user_id } yoki { order_id } yoki boshqa
  };
}

export interface CreateTransactionDto extends PaymeRequestBase {
  params: {
    id: string; // Payme transaction ID
    time: number; // ms timestamp
    amount: number; // tiyn
    account: Record<string, any>;
  };
}

export interface PerformTransactionDto extends PaymeRequestBase {
  params: { id: string };
}

export interface CancelTransactionDto extends PaymeRequestBase {
  params: { id: string; reason: number };
}

export interface CheckTransactionDto extends PaymeRequestBase {
  params: { id: string };
}

export interface GetStatementDto extends PaymeRequestBase {
  params: { from: number; to: number }; // ms timestamps
}

export type RequestBody =
  | CheckPerformTransactionDto
  | CreateTransactionDto
  | PerformTransactionDto
  | CancelTransactionDto
  | CheckTransactionDto
  | GetStatementDto;

Payme Guard (Basic Auth)

// src/auth/guards/payme.guard.ts
@Injectable()
export class PaymeBasicAuthGuard implements CanActivate {
  constructor(private readonly config: ConfigService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest<Request>();
    const res = context.switchToHttp().getResponse<Response>();
    const id = req?.body?.id;

    const sendError = () => {
      res.status(200).json({ id, error: PaymeError.InvalidAuthorization });
      return false;
    };

    const [type, token] = req.headers["authorization"]?.split(" ") ?? [];
    if (type !== "Basic" || !token) return sendError();

    const decoded = atob(token); // "Paycom:SECRET_KEY"
    const [username, password] = decoded.split(":");

    if (
      this.config.get("PAYME_LOGIN") !== username ||
      this.config.get("PAYME_PASSWORD") !== password
    )
      return sendError();

    return true;
  }
}

Payme Service Pattern

@Injectable()
export class PaymeService {
  private readonly logger = new Logger(PaymeService.name);

  async handleTransactionMethods(body: RequestBody) {
    this.logger.log(`Payme method: ${body.method} id=${body.id}`);
    switch (body.method) {
      case TransactionMethods.CheckPerformTransaction:
        return this.checkPerformTransaction(body as CheckPerformTransactionDto);
      case TransactionMethods.CreateTransaction:
        return this.createTransaction(body as CreateTransactionDto);
      case TransactionMethods.CheckTransaction:
        return this.checkTransaction(body as CheckTransactionDto);
      case TransactionMethods.PerformTransaction:
        return this.performTransaction(body as PerformTransactionDto);
      case TransactionMethods.CancelTransaction:
        return this.cancelTransaction(body as CancelTransactionDto);
      case TransactionMethods.GetStatement:
        return this.getStatement(body as GetStatementDto);
      default:
        return { error: PaymeError.CantDoOperation, id: (body as any).id };
    }
  }

  private async checkPerformTransaction(dto: CheckPerformTransactionDto) {
    // 1. Find entity by account params — adapt to your project
    const entity = await this.findEntityByAccount(dto.params.account);
    if (!entity) return { error: PaymeError.AccountNotFound, id: dto.id };

    // 2. Validate amount
    if (dto.params.amount <= 0)
      return { error: PaymeError.InvalidAmount, id: dto.id };

    // 3. Optional: extra business validation (e.g. already subscribed, order status)

    return {
      result: {
        allow: true,
        additional: {
          // Return info shown in Payme checkout — adapt fields
          name: entity.name?.slice(0, 20),
        },
      },
    };
  }

  private async createTransaction(dto: CreateTransactionDto) {
    const transId = dto.params.id;

    const existing = await this.findTransactionById(transId);
    if (existing?.status === "PAID")
      return {
        result: {
          transaction: transId,
          state: TransactionState.Paid, // PAID → Paid (bug fix)
          create_time: +existing.createdAt,
        },
        id: dto.id,
      };
    if (existing?.status === "PENDING")
      return {
        result: {
          transaction: transId,
          state: TransactionState.Pending,
          create_time: +existing.createdAt,
        },
        id: dto.id,
      };
    if (existing?.status === "CANCELED")
      return { error: PaymeError.CantDoOperation, id: dto.id };

    const entity = await this.findEntityByAccount(dto.params.account);
    if (!entity) return { error: PaymeError.AccountNotFound, id: dto.id };

    const tx = await this.saveTransaction({
      transactionId: transId,
      entityId: this.extractEntityId(dto.params.account),
      amount: dto.params.amount / 100, // tiyn → UZS; store in UZS
      status: "PENDING",
      state: TransactionState.Pending,
      provider: "PAYME",
    });

    return {
      result: {
        transaction: transId,
        state: TransactionState.Pending,
        create_time: +tx.updatedAt,
      },
      id: dto.id,
    };
  }

  private async checkTransaction(dto: CheckTransactionDto) {
    const tx = await this.findTransactionById(dto.params.id);
    if (!tx)
      return { error: PaymeError.TransactionNotFound, id: dto.params.id };

    return {
      result: {
        create_time: +tx.createdAt,
        perform_time: tx.performTime ? +tx.performTime : 0,
        cancel_time: tx.cancelTime ? +tx.cancelTime : 0,
        transaction: tx.transactionId,
        state: tx.state,
        reason: tx.reason ?? null,
      },
    };
  }

  private async performTransaction(dto: PerformTransactionDto) {
    const tx = await this.findTransactionById(dto.params.id);
    if (!tx)
      return { error: PaymeError.TransactionNotFound, id: dto.params.id };

    if (tx.status === "PAID")
      return {
        result: {
          transaction: tx.transactionId,
          perform_time: tx.performTime ? +tx.performTime : 0,
          state: TransactionState.Paid,
        },
      };
    if (tx.status === "CANCELED")
      return { error: PaymeError.CantDoOperation, id: dto.params.id };

    const updated = await this.markTransactionPaid(tx.id);
    await this.onPaymentSuccess(tx.id); // your business logic

    return {
      result: {
        transaction: updated.transactionId,
        perform_time: updated.performTime ? +updated.performTime : 0,
        state: TransactionState.Paid,
      },
    };
  }

  private async cancelTransaction(dto: CancelTransactionDto) {
    const tx = await this.findTransactionById(dto.params.id);
    if (!tx)
      return { error: PaymeError.TransactionNotFound, id: dto.params.id };

    if (tx.status === "PENDING") {
      const cancelTime = new Date();
      await this.markTransactionCanceled(
        tx.id,
        TransactionState.PendingCanceled,
        CancelingReasons.TransactionFailed,
      );
      return {
        result: {
          cancel_time: +cancelTime,
          transaction: tx.transactionId,
          state: TransactionState.PendingCanceled,
        },
      };
    }

    if (tx.status === "PAID") {
      // Check if refund is possible (business logic)
      const canRefund = await this.checkRefundPossibility(tx);
      if (!canRefund)
        return { error: PaymeError.CantDoOperation, id: dto.params.id };

      const cancelTime = new Date();
      await this.onRefund(tx); // reverse business logic
      await this.markTransactionCanceled(
        tx.id,
        TransactionState.PaidCanceled,
        CancelingReasons.Refund,
      );
      return {
        result: {
          cancel_time: +cancelTime,
          transaction: tx.transactionId,
          state: TransactionState.PaidCanceled,
        },
      };
    }

    // Already canceled
    return {
      result: {
        cancel_time: tx.cancelTime ? +tx.cancelTime : 0,
        state: tx.state,
        transaction: tx.transactionId,
      },
    };
  }

  private async getStatement(dto: GetStatementDto) {
    const transactions = await this.findTransactionsByDateRange(
      new Date(dto.params.from),
      new Date(dto.params.to),
      "PAYME",
    );

    return {
      result: {
        transactions: transactions.map((tx) => ({
          id: tx.transactionId,
          account: tx.entityId,
          amount: Number(tx.amount) * 100, // UZS → tiyn for Payme
          create_time: +tx.createdAt,
          perform_time: tx.performTime ? +tx.performTime : 0,
          cancel_time: tx.cancelTime ? +tx.cancelTime : 0,
          state: tx.state,
          reason: tx.reason ?? null,
        })),
      },
    };
  }
}

Payment Controller

@Controller("payment")
export class PaymentController {
  constructor(
    private readonly paymeService: PaymeService,
    private readonly clickService: ClickService,
  ) {}

  @Post("payme")
  @UseGuards(PaymeBasicAuthGuard)
  @HttpCode(HttpStatus.OK) // ALWAYS 200 for Payme!
  handlePayme(@Body() body: RequestBody) {
    return this.paymeService.handleTransactionMethods(body);
  }

  @Post("click")
  @HttpCode(HttpStatus.OK)
  handleClick(@Req() req: Request) {
    return this.clickService.handleWebhook(req.body);
  }

  // Optional — only if merchant agreement requires GetInfo
  @Post("click/getinfo")
  @HttpCode(200)
  getClickInfo(@Body() body: any) {
    return this.clickService.getInfo(body);
  }
}

Prisma Transaction Model Example

Adapt to your project — these are the minimum fields needed:

model Payment {
  id            String   @id @default(cuid())
  transactionId String   @unique      // Payme/Click transaction ID
  prepareId     String?               // Click only: your server-generated timestamp
  provider      String                // "PAYME" | "CLICK"
  status        String   @default("PENDING")  // PENDING | PAID | CANCELED
  state         Int?                  // Payme state (-2, -1, 1, 2)
  amount        Decimal               // Store in UZS
  reason        Int?                  // Payme cancel reason
  entityId      String                // your user/order/product ID
  performTime   DateTime?
  cancelTime    DateTime?
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

Critical Rules

Setup (bir marta)

// main.ts — ValidationPipe global yoqilmasa DTO validatsiyasi ishlamaydi
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));

Payme

  • HTTP response har doim 200 — xatolar body'da { error: {...} } ko'rinishida
  • Amount tiynda keladi (÷ 100 → DB ga UZS saqla, × 100 → GetStatement da qaytarib ber)
  • id response'da request'dagi id bilan mos bo'lishi shart (JSON-RPC qoidasi)
  • Guard: atob(token)"Paycom:SECRET_KEY" formatida decode qiladi

Click

  • Signature har qanday DB operatsiyadan oldin tekshirilsin
  • merchantPrepareIdprepare hash'da YO'Q, complete hash'da BOR
  • body.error > 0 complete'da = Click'dan xato → tranzaktsiyani bekor qil
  • amount string sifatida kelishi mumkin — har doim parseFloat() qil
  • prepareIdsiz generate qilasiz (masalan Date.now())

Ikkala tizim uchun

  • Idempotency: har doim dublikat tranzaktsiyani tekshiring
  • onPaymentSuccess va onRefund — business logikangizni alohida method'da saqlang
  • Barcha webhook body'larini log qiling
  • ID type'larini loyihadagi real type'ga moslang (string, number, yoki boshqa)

Official Documentation

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

OpenClaw Mobile Gateway Installer

Installs and manages OpenClaw mobile gateway as a system service. Invoke when users need one-command deploy, start, stop, upgrade, or uninstall.

Registry SourceRecently Updated
Coding

Agent Stack Picker

Recommend a minimal, battle-tested stack for AI-agent-driven product development. Use when choosing technology for web, mobile, desktop, backend, automation,...

Registry SourceRecently Updated
Coding

Github Actions Gen

Unknown: help. Use when you need github actions gen capabilities. Triggers on: github actions gen, type, lang, deploy, matrix, no-cache.

Registry SourceRecently Updated
1080ckchzh
Coding

Fontpick

Font pairing and typography helper for designers and developers. Get selectd font pairings by style (modern, classic, minimal, bold, code), generate CSS font...

Registry SourceRecently Updated