{
  "slug": "Integrating-Zarinpal-Payment-Gateway-in-NestJS-12017908490d",
  "title": "Integrating Zarinpal Payment Gateway in NestJS",
  "subtitle": "A guide on how to integrate Zarinpal Payment Gateway in NestJS.",
  "excerpt": "A guide on how to integrate Zarinpal Payment Gateway in NestJS.",
  "date": "2024-08-01",
  "tags": [
    "Node.js",
    "Payments"
  ],
  "readingTime": "4 min",
  "url": "https://medium.com/@mobinshaterian/integrating-zarinpal-payment-gateway-in-nestjs-12017908490d",
  "hero": "https://cdn-images-1.medium.com/max/800/1*hsp-3yoQce39agYt5dyBTQ.jpeg",
  "content": [
    {
      "type": "heading",
      "level": 2,
      "text": "Integrating Zarinpal Payment Gateway in NestJS"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "A guide on how to integrate Zarinpal Payment Gateway in NestJS."
    },
    {
      "type": "paragraph",
      "html": "The provided content outlines integrating the Zarinpal payment gateway into a NestJS application. Zarinpal is an Iranian payment service provider, and this implementation allows developers to facilitate online transactions in their web applications."
    },
    {
      "type": "paragraph",
      "html": "The integration process begins with installing the necessary package, “zarinpal-nestjs”, and setting up the module in the application. The app module and configuration files specify configuration details, such as the merchant ID and callback URL."
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*hsp-3yoQce39agYt5dyBTQ.jpeg",
      "alt": "Integrating Zarinpal Payment Gateway in NestJS",
      "caption": "",
      "width": 1024,
      "height": 1024
    },
    {
      "type": "paragraph",
      "html": "The payment flow consists of two main steps: opening and verifying a transaction. The “<strong>openTransaction</strong>” function initiates the payment process, creating a link redirecting the user to the bank’s payment page. After the user completes the payment, they are redirected to the application’s callback URL."
    },
    {
      "type": "paragraph",
      "html": "The “<strong>verifyTransaction</strong>” function is crucial for confirming the payment’s success or failure. It checks the payment status and communicates with Zarinpal’s API to verify the transaction details. The implementation includes error handling and updates the payment status in the database accordingly."
    },
    {
      "type": "paragraph",
      "html": "The code snippets provided demonstrate the structure of the payment module, including the controller, service, and data transfer objects (DTOs). They also showcase the use of MongoDB with Mongoose to store payment information."
    },
    {
      "type": "paragraph",
      "html": "One notable point is mentioning a bug in the package’s <strong>verifyTransaction</strong> function. The implementation includes a custom verification method using direct HTTP requests to Zarinpal’s API to work around this."
    },
    {
      "type": "paragraph",
      "html": "In conclusion, this integration offers a comprehensive solution for handling online payments in a NestJS application, covering the entire payment lifecycle from initiation to verification. It provides a solid foundation for developers looking to implement Zarinpal as their payment gateway in Node.js-based web applications."
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Create a resource"
    },
    {
      "type": "code",
      "lang": "text",
      "code": "nest g resource payment"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Nodejs sample code"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Nestjs sample code"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Documentation"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Installation"
    },
    {
      "type": "code",
      "lang": "bash",
      "code": "yarn add zarinpal-nestjs\nnpm install --save zarinpal-nestjs\nnpm audit fix --force"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Open Transaction"
    },
    {
      "type": "paragraph",
      "html": "In the first step, it is mandatory to initial a transaction."
    },
    {
      "type": "heading",
      "level": 3,
      "text": "app.module.ts"
    },
    {
      "type": "code",
      "lang": "text",
      "code": "ZarinpalModule.register({      callBackUrl: configuration().zarinpal.callBackUrl,      merchantId:\nconfiguration().zarinpal.merchantId,    }),    PaymentModule,"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "configuration.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "zarinpal: {    merchantId: process.env.ZARINPAL_MERCHANT_ID,    callBackUrl:\nprocess.env.ZARINPAL_CALLBACK_URL,  },"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "payment.controller.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "@Post('/openTransaction/') @UseGuards(CustomerGuard) @ApiOkResponse({\n  type: () => CreatePaymentResultDto\n}) @ApiBearerAuth() create(@Body() createPaymentDto: CreatePaymentDto, @Req() req: {\n  user: Partial<User>\n},) {\n  return this.paymentService.openTransaction(createPaymentDto, req.user);\n}"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "payment.service.ts"
    },
    {
      "type": "code",
      "lang": "javascript",
      "code": "async openTransaction(    createPaymentDto: CreatePaymentDto,    user: Partial<User>,  ):\nPromise<CreatePaymentResultDto> {    const transactionResult = await\nthis.zarinpalService.openTransaction({      amount: createPaymentDto.amount,      description:\ncreatePaymentDto.uniqueID,      metadata: {        mobile: user.phoneNumber,      },    });    try {\nconst result =        this.zarinpalService.generateStartPayUrl(transactionResult);      await\nthis.paymentModel.create({        user: user,        amount: createPaymentDto.amount,\nuniqueID: createPaymentDto.uniqueID,        status: PaymentStatus.Initial,        gateway:\nPaymentGateway.Zarinpal,        paymentLink: result,        authority: transactionResult,      });\nreturn { paymentLink: result };    } catch (e) {      throw InternalServerErrorException;    }  }"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "create.dto.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  ApiProperty\n}\nfrom '@nestjs/swagger';\nexport class CreatePaymentDto {\n  @ApiProperty() amount: number;\n  @ApiProperty() uniqueID: string;\n}\nexport class CreatePaymentResultDto {\n  @ApiProperty() paymentLink: string;\n}"
    },
    {
      "type": "paragraph",
      "html": "<strong>payment.entity.ts</strong>"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Prop, Schema, SchemaFactory\n}\nfrom '@nestjs/mongoose';\nimport {\n  ApiProperty, ApiPropertyOptional\n}\nfrom '@nestjs/swagger';\nimport mongoose, {\n  Document\n}\nfrom 'mongoose';\nimport {\n  User\n}\nfrom 'src/users/schemas/user.schema';\nexport enum PaymentStatus {\n  Initial = 'Initial', Pending = 'Pending', Success = 'Success', Failed = 'Failed',\n}\nexport enum PaymentGateway {\n  Zarinpal = 'Zarinpal',\n}\n@Schema({\n  timestamps: true, collection: 'Payment',\n})\nexport class Payment extends Document {\n  @ApiProperty() _id: string;\n  @ApiProperty({\n    type: () => User\n  }) @Prop({\n    type: mongoose.Schema.Types.ObjectId, ref: 'user'\n  }) user: User;\n  @ApiProperty() @Prop({\n    required: true\n  }) amount: number;\n  @ApiProperty() @Prop({\n    required: true, unique: true\n  }) uniqueID: string;\n  @ApiProperty({\n    type: () => [PaymentStatus]\n  }) @Prop({\n    required: true\n  }) status: string;\n  @ApiProperty({\n    type: () => [PaymentGateway]\n  }) @Prop({\n    required: true\n  }) gateway: string;\n  @ApiProperty() @Prop({\n    required: true\n  }) paymentLink: string;\n  @ApiPropertyOptional() @Prop({\n    type: Object\n  }) authority: Record<string, any>;\n}\nconst PaymentSchema = SchemaFactory.createForClass(Payment);\nexport {\n  PaymentSchema\n};"
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*XCNXf75ifIou7diVH7TSSg.png",
      "alt": "Integrating Zarinpal Payment Gateway in NestJS",
      "caption": "",
      "width": 607,
      "height": 423
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Payment"
    },
    {
      "type": "paragraph",
      "html": "the client will go to the bank page and pay with his cart. Then, the bank sends him back to the callback on the website."
    },
    {
      "type": "image",
      "src": "https://cdn-images-1.medium.com/max/800/1*5SVnK5SggRfzmSMxAp7q2g.png",
      "alt": "Integrating Zarinpal Payment Gateway in NestJS",
      "caption": "",
      "width": 880,
      "height": 574
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Verify Transaction"
    },
    {
      "type": "paragraph",
      "html": "If you don’t verify the transaction after 30 minutes, money will be returned to the customer."
    },
    {
      "type": "heading",
      "level": 3,
      "text": "payment.controler.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "@Get('/zarinpal/verify') @ApiOkResponse({\n  type: () => VerifyPaymentZarinPalResponseDto\n}) async verifyTransaction(@Query() query: VerifyPaymentZarinPalDto) {\n  return this.paymentZarinpalService.verifyTransaction(query);\n}"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "payment.module.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "import {\n  Module\n}\nfrom '@nestjs/common';\nimport {\n  PaymentZarinpalService\n}\nfrom './payment.zarinpal.service';\nimport {\n  PaymentController\n}\nfrom './payment.controller';\nimport {\n  AuthModule\n}\nfrom 'src/auth/auth.module';\nimport {\n  MongooseModule\n}\nfrom '@nestjs/mongoose';\nimport {\n  Payment, PaymentSchema\n}\nfrom './entities/payment.entity';\nimport {\n  HttpModule\n}\nfrom '@nestjs/axios';\n@Module({\n  imports: [AuthModule, MongooseModule.forFeature([{\n    name: Payment.name, schema: PaymentSchema\n  }]), HttpModule,], controllers: [PaymentController], providers: [PaymentZarinpalService],\n})\nexport class PaymentModule {\n}"
    },
    {
      "type": "heading",
      "level": 3,
      "text": "payment.zarinpal.service."
    },
    {
      "type": "paragraph",
      "html": "In this case, it is possible to use a verifyTransaction function."
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "async verifyTransaction(query: VerifyPaymentZarinPalDto) {    const payment = await\nthis.paymentModel.findOne({      authority_code: query.Authority,    });    if (!payment) {\nthrow new NotFoundException();    }    if (      payment.status == PaymentStatus.Failed ||\npayment.status == PaymentStatus.Success    ) {      return new NotFoundException();    }    if\n(query.Status === 'NOK') {      payment.status = PaymentStatus.Failed;      payment.save();\nreturn { status: PaymentStatus.Failed, message: PaymentStatus.Failed };    }    payment.status =\nPaymentStatus.Pending;    await payment.save();    try {      const verify = await\nthis.zarinpalService.verifyTransaction({        authority: query.Authority,        amount:\npayment.amount,      });      if (verify.code == 100 || verify.code == 101) {        payment.status\n= PaymentStatus.Success;      } else {        payment.status = PaymentStatus.Failed;      }\npayment.response = verify;      await payment.save();      return { status: payment.status, message:\nverify.message };    } catch (e) {      console.error(e);      payment.response = e;\npayment.status = PaymentStatus.Failed;      payment.save();      throw InternalServerErrorException;\n}  }"
    },
    {
      "type": "paragraph",
      "html": "create.dto.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "export class VerifyPaymentZarinPalDto {\n  @ApiProperty() Status: string;\n  @ApiProperty() Authority: string;\n}\nexport class VerifyPaymentZarinPalResponseDto {\n  @ApiProperty() Status: string;\n  @ApiProperty() Message: string;\n}"
    },
    {
      "type": "paragraph",
      "html": "zarinpal.type.ts"
    },
    {
      "type": "code",
      "lang": "typescript",
      "code": "export class ZarinPalVerifyResponse {\n  data: {\n    code: number;\n    message: string;\n    card_hash: string;\n    card_pan: string;\n    ref_id: number;\n    fee_type: string;\n    fee: number;\n    shaparak_fee: string;\n    order_id: number | null;\n  };\n  errors: any[];\n}\nexport const ZarinPalVerifyURL = 'https://payment.zarinpal.com/pg/v4/payment/verify.json';"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Stackademic 🎓"
    },
    {
      "type": "paragraph",
      "html": "Thank you for reading until the end. Before you go:"
    },
    {
      "type": "list",
      "ordered": false,
      "items": [
        "Please consider <strong>clapping</strong> and <strong>following</strong> the writer! 👏",
        "Follow us <a href=\"https://twitter.com/stackademichq\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>X</strong></a> | <a href=\"https://www.linkedin.com/company/stackademic\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>LinkedIn</strong></a> | <a href=\"https://www.youtube.com/c/stackademic\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>YouTube</strong></a> | <a href=\"https://discord.gg/in-plain-english-709094664682340443\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Discord</strong></a>",
        "Visit our other platforms: <a href=\"https://plainenglish.io/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>In Plain English</strong></a> | <a href=\"https://cofeed.app/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>CoFeed</strong></a> | <a href=\"https://differ.blog/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Differ</strong></a>",
        "More content at <a href=\"https://stackademic.com/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Stackademic.com</strong></a>"
      ]
    }
  ]
}
