All files / src/modules/billing click.controller.ts

34.48% Statements 10/29
40% Branches 8/20
33.33% Functions 1/3
29.62% Lines 8/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 12511x 11x 11x                                           11x   11x 11x         11x                                                                                         11x                                                                                              
import { Body, Controller, Headers, HttpCode, Post } from '@nestjs/common';
import { BillingService } from '@app/modules/billing/billing.service';
import { ClickSigningService } from '@app/modules/billing/click.signing.service';
 
class ClickBaseDto {
  click_trans_id!: string;
  service_id!: number;
  click_paydoc_id!: string;
  merchant_trans_id!: string;
  amount!: number;
  action!: number; // 0 prepare, 1 complete
  error!: number;
  error_note!: string;
  sign_time!: string;
  sign_string!: string;
}
 
class ClickPrepareDto extends ClickBaseDto {}
 
class ClickCompleteDto extends ClickBaseDto {
  merchant_prepare_id!: number;
}
 
@Controller('billing/click')
export class ClickController {
  constructor(
    private readonly billing: BillingService,
    private readonly signing: ClickSigningService,
  ) {}
 
  @Post('prepare')
  @HttpCode(200)
  async prepare(@Body() p: ClickPrepareDto, @Headers() _headers: Record<string, string>) {
    // Basic validation of content-type is optional; Nest parses form urlencoded via Fastify
    Iif (!this.signing.verifyPrepareSig(p)) {
      return {
        click_trans_id: p.click_trans_id,
        merchant_trans_id: p.merchant_trans_id,
        merchant_prepare_id: null,
        error: -3,
        error_note: 'invalid_sign_string',
      };
    }
    // Defer state checks to service function; reuse existing payment lookup
    const payment = await this.billing.getPaymentByMerchantTransId(p.merchant_trans_id);
    Iif (!payment) {
      return {
        click_trans_id: p.click_trans_id,
        merchant_trans_id: p.merchant_trans_id,
        merchant_prepare_id: null,
        error: -5,
        error_note: 'order_not_found',
      };
    }
    Iif (payment.amountCents !== Math.round(p.amount * 100)) {
      return {
        click_trans_id: p.click_trans_id,
        merchant_trans_id: p.merchant_trans_id,
        merchant_prepare_id: null,
        error: -2,
        error_note: 'incorrect_amount',
      };
    }
    // Update status to PREPARED and return merchant_prepare_id (use internal id)
    // @ts-ignore
    await (this.billing as any).prisma.client.payment.update({ where: { id: payment.id }, data: { status: 'PREPARED', merchantPrepareId: payment.id } });
    return {
      click_trans_id: p.click_trans_id,
      merchant_trans_id: p.merchant_trans_id,
      merchant_prepare_id: payment.id,
      error: 0,
      error_note: 'Success',
    };
  }
 
  @Post('complete')
  @HttpCode(200)
  async complete(@Body() p: ClickCompleteDto) {
    Iif (!this.signing.verifyCompleteSig(p)) {
      return {
        click_trans_id: p.click_trans_id,
        merchant_trans_id: p.merchant_trans_id,
        merchant_confirm_id: null,
        error: -3,
        error_note: 'invalid_sign_string',
      };
    }
    const payment = await this.billing.getPaymentByMerchantTransId(p.merchant_trans_id);
    Iif (!payment) {
      return {
        click_trans_id: p.click_trans_id,
        merchant_trans_id: p.merchant_trans_id,
        merchant_confirm_id: null,
        error: -5,
        error_note: 'order_not_found',
      };
    }
    // If error from provider, mark failed
    Iif (p.error && p.error <= -1) {
      // @ts-ignore
      await (this.billing as any).prisma.client.payment.update({ where: { id: payment.id }, data: { status: 'FAILED' } });
      return {
        click_trans_id: p.click_trans_id,
        merchant_trans_id: p.merchant_trans_id,
        merchant_confirm_id: payment.id,
        error: 0,
        error_note: 'Success',
      };
    }
    // Success path: confirm
    // @ts-ignore
    await (this.billing as any).prisma.client.payment.update({ where: { id: payment.id }, data: { status: 'CONFIRMED', clickTransId: p.click_trans_id, clickPaydocId: p.click_paydoc_id, merchantConfirmId: payment.id } });
    // TODO: enqueue entitlement job
    return {
      click_trans_id: p.click_trans_id,
      merchant_trans_id: p.merchant_trans_id,
      merchant_confirm_id: payment.id,
      error: 0,
      error_note: 'Success',
    };
  }
}