To'lov tizimlari — Click / Payme / Stripe

~35 daq · NestJS · Qo'shimcha xizmatlar

Muammodan boshlaylik

Siz e-commerce ilova yozdingiz. Foydalanuvchi mahsulot tanladi, "To'lash" tugmasini bosdi. Endi nima bo'ladi?

Pul to'g'ridan-to'g'ri sizning serveringizga kelmaydi. O'rtada to'lov tizimi turadi:

Foydalanuvchi → Click/Payme/Stripe → Sizning server
                      ↓
               Bank karta tekshiruvi
               Pul o'tkazish
               Sizga xabar berish

To'lov tizimi:

  • Karta ma'lumotlarini xavfsiz saqlaydi (siz saqlamaysiz)
  • Bank bilan aloqa qiladi
  • Pul o'tganini sizga bildiradi

O'zbekistondagi to'lov tizimlari

Click va Payme — O'zbekistondagi eng mashhur to'lov tizimlari.

Ikkalasi ham xuddi shunday ishlaydi:

  1. Siz buyurtma yaratasiz (amount, orderId)
  2. Foydalanuvchi ularning sahifasiga o'tadi, to'laydi
  3. Ular sizning serveringizga Prepare va Complete so'rovlar yuboradi
  4. Siz tasdiqlaysiz — pul hisobingizga o'tadi

Payme integratsiyasi

Merchant API ni tushunish

Payme 2 ta so'rov yuboradi:

CheckPerformTransaction  → Buyurtma to'lanishi mumkinmi?
CreateTransaction        → Tranzaksiya yaratildi
PerformTransaction       → To'lov muvaffaqiyatli, pul o'tkazildi
CancelTransaction        → Bekor qilindi

O'rnatish

npm install @nestjs/axios axios

Controller

// payment/payme.controller.ts
@Controller('payment/payme')
export class PaymeController {
  constructor(private paymeService: PaymeService) {}

  @Post()
  async handle(@Body() body: any, @Headers('authorization') auth: string) {
    // Autentifikatsiya tekshiruvi
    if (!this.paymeService.checkAuth(auth)) {
      return { error: { code: -32504, message: 'Insufficient privilege' } };
    }

    const { method, params, id } = body;

    switch (method) {
      case 'CheckPerformTransaction':
        return this.paymeService.checkPerform(params, id);
      case 'CreateTransaction':
        return this.paymeService.createTransaction(params, id);
      case 'PerformTransaction':
        return this.paymeService.performTransaction(params, id);
      case 'CancelTransaction':
        return this.paymeService.cancelTransaction(params, id);
      default:
        return { error: { code: -32601, message: 'Method not found' }, id };
    }
  }
}

Service

// payment/payme.service.ts
@Injectable()
export class PaymeService {

  checkAuth(authHeader: string): boolean {
    // Basic auth: "Basic base64(login:password)"
    const expected = Buffer.from(
      `Paycom:${process.env.PAYME_KEY}`
    ).toString('base64');
    return authHeader === `Basic ${expected}`;
  }

  async checkPerform(params: any, id: string) {
    const { amount, account } = params;
    const orderId = account.order_id;

    const order = await this.ordersService.findOne(orderId);

    if (!order) {
      return {
        result: null,
        error: { code: -31050, message: 'Order not found' },
        id,
      };
    }

    // Summa mos kelishini tekshirish (tiyin da)
    if (order.amount * 100 !== amount) {
      return {
        result: null,
        error: { code: -31001, message: 'Wrong amount' },
        id,
      };
    }

    return { result: { allow: true }, id };
  }

  async performTransaction(params: any, id: string) {
    const { id: transactionId, account } = params;
    const orderId = account.order_id;

    // Buyurtmani to'langan deb belgilash
    await this.ordersService.markAsPaid(orderId, transactionId);

    return {
      result: {
        transaction: transactionId,
        perform_time: Date.now(),
        state: 2, // to'langan
      },
      id,
    };
  }
}

Click integratsiyasi

Click ikki endpoint bilan ishlaydi:

// payment/click.controller.ts
@Controller('payment/click')
export class ClickController {

  // 1. Prepare — to'lov boshlanishidan oldin tekshirish
  @Post('prepare')
  async prepare(@Body() body: ClickPrepareDto) {
    const { merchant_trans_id, amount, sign_time, sign_string } = body;

    // Imzoni tekshirish
    const expectedSign = this.clickService.generateSign(body);
    if (sign_string !== expectedSign) {
      return { error: -1, error_note: 'Sign check failed!' };
    }

    const order = await this.ordersService.findOne(merchant_trans_id);
    if (!order) {
      return { error: -5, error_note: 'Order not found' };
    }

    return {
      click_trans_id: body.click_trans_id,
      merchant_trans_id,
      merchant_prepare_id: order.id,
      error: 0,
      error_note: 'Success',
    };
  }

  // 2. Complete — to'lov tugagandan keyin
  @Post('complete')
  async complete(@Body() body: ClickCompleteDto) {
    const { merchant_trans_id, error } = body;

    if (error < 0) {
      // To'lov bekor qilindi
      await this.ordersService.markAsCancelled(merchant_trans_id);
      return { error: 0, error_note: 'Cancelled' };
    }

    // Muvaffaqiyatli to'lov
    await this.ordersService.markAsPaid(merchant_trans_id);

    return { error: 0, error_note: 'Success' };
  }
}

Stripe integratsiyasi (xalqaro)

Stripe — xalqaro to'lovlar uchun. AQSh, Yevropa bozori.

npm install stripe
// payment/stripe.service.ts
import Stripe from 'stripe';

@Injectable()
export class StripeService {
  private stripe: Stripe;

  constructor() {
    this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
  }

  // To'lov sahifasi URL yaratish
  async createCheckoutSession(orderId: string, amount: number) {
    const session = await this.stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: [{
        price_data: {
          currency: 'usd',
          product_data: { name: `Order #${orderId}` },
          unit_amount: amount * 100, // cent da
        },
        quantity: 1,
      }],
      mode: 'payment',
      success_url: `${process.env.FRONTEND_URL}/payment/success?orderId=${orderId}`,
      cancel_url: `${process.env.FRONTEND_URL}/payment/cancel`,
    });

    return { url: session.url }; // ← foydalanuvchini shu URLga yo'naltiring
  }
}
// payment/stripe-webhook.controller.ts
@Controller('payment/stripe/webhook')
export class StripeWebhookController {

  @Post()
  async webhook(
    @Req() req: RawBodyRequest<Request>,
    @Headers('stripe-signature') sig: string,
  ) {
    let event: Stripe.Event;

    try {
      event = this.stripe.webhooks.constructEvent(
        req.rawBody,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET,
      );
    } catch {
      throw new BadRequestException('Webhook signature error');
    }

    if (event.type === 'checkout.session.completed') {
      const session = event.data.object as Stripe.Checkout.Session;
      const orderId = session.metadata.orderId;
      await this.ordersService.markAsPaid(orderId);
    }
  }
}

⚠️ Stripe webhook uchun raw body kerak. main.ts da: app.use('/payment/stripe/webhook', express.raw({ type: 'application/json' }))


To'lov oqimi — umumiy

1. Foydalanuvchi "To'lash" tugmasini bosdi
   → POST /orders → order yaratildi (status: PENDING)

2. To'lov sahifasiga yo'naltirildi
   → Click/Payme/Stripe sahifasi

3. Foydalanuvchi to'ladi
   → To'lov tizimi sizning serverga webhook yubordi

4. Siz tasdiqlading
   → order.status = PAID
   → Email/Telegram xabar

5. Foydalanuvchi success sahifaga qaytdi

Tez-tez uchraydigan xatolar

1. Sign mismatch

error: -1, error_note: "Sign check failed"

Sababi: Imzoni hisoblashda maydonlar tartibi yoki kalit noto'g'ri. Yechim: Dokumentatsiyadan imzo formulasini qayta tekshiring.


2. Webhook ikki marta keldi Sababi: To'lov tizimlari xato bo'lsa qayta yuboradi. Yechim: transactionId ga unique constraint qo'ying — ikki marta qabul qilinmasin.


3. Test rejimida ishlayapti, prod da yo'q Sababi: Test API key va prod API key boshqacha. Yechim: .env.production da prod keylarni sozlang.


Xulosa

Eslab qoling:

  • To'lov tizimlari webhook yuboradi — siz eshitasiz
  • Payme: CheckPerform → CreateTransaction → PerformTransaction
  • Click: Prepare → Complete
  • Stripe: Checkout Session → Webhook
  • Webhook xabarlar takrorlanishi mumkin — idempotent qiling
  • Hech qachon karta ma'lumotlarini o'zingiz saqlamang

Keyingi dars: 52-dars: Swagger — API dokumentatsiya