diff --git a/env/env.development b/env/env.development index 4a94509..db61ef4 100644 --- a/env/env.development +++ b/env/env.development @@ -22,10 +22,15 @@ CRON_MIDNIGHT="55 11 * * *" CRON_EVERY_MINUTE="55 11 * * *" CRON_EVERY_HOUR="0 * * * *" +EMAIL_HOST="sandbox.smtp.mailtrap.io" +EMAIL_POST=465 +EMAIL_USER="developer@eigen.co.id" +EMAIL_TOKEN="bitqkbkzjzfywxqx" + MIDTRANS_URL=https://app.sandbox.midtrans.com MIDTRANS_PRODUCTION=false -MIDTRANS_SERVER_KEY=SB-Mid-server-ITmSD6C0nXfIcmgi4TXm6J7i -MIDTRANS_CLIENT_KEY=SB-Mid-client-VFaU_cPL6kh2DKir +MIDTRANS_SERVER_KEY= +MIDTRANS_CLIENT_KEY= EXPORT_LIMIT_PARTITION=200 ASSETS="https://asset.sky.eigen.co.id/" \ No newline at end of file diff --git a/env/env.production b/env/env.production index e0019f2..b389dbc 100644 --- a/env/env.production +++ b/env/env.production @@ -22,10 +22,15 @@ CRON_MIDNIGHT="55 11 * * *" CRON_EVERY_MINUTE="55 11 * * *" CRON_EVERY_HOUR="0 * * * *" +EMAIL_HOST="sandbox.smtp.mailtrap.io" +EMAIL_POST=465 +EMAIL_USER= +EMAIL_TOKEN= + MIDTRANS_URL=https://app.midtrans.com MIDTRANS_PRODUCTION=true -MIDTRANS_SERVER_KEY=Mid-server-6lA4Nnmov2BSOcwVq1sLSOpC -MIDTRANS_CLIENT_KEY=Mid-client-JiPIkIPd_RGooF8U +MIDTRANS_SERVER_KEY= +MIDTRANS_CLIENT_KEY= EXPORT_LIMIT_PARTITION=200 ASSETS="https://asset.sky.eigen.co.id/" \ No newline at end of file diff --git a/package.json b/package.json index f97ceb7..944ef0f 100644 --- a/package.json +++ b/package.json @@ -46,10 +46,12 @@ "elastic-apm-node": "^4.5.4", "exceljs": "^4.4.0", "googleapis": "^140.0.0", + "handlebars": "^4.7.8", "mathjs": "^13.0.2", "midtrans-client": "^1.3.1", "moment": "^2.30.1", "nano": "^10.1.3", + "nodemailer": "^6.9.14", "pg": "^8.11.5", "plop": "^4.0.1", "reflect-metadata": "^0.2.0", diff --git a/src/app.module.ts b/src/app.module.ts index 36258c3..08b50d9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -68,6 +68,7 @@ import { NewsModule } from './modules/web-information/news/news.module'; import { NewsModel } from './modules/web-information/news/data/models/news.model'; import { BannerModule } from './modules/web-information/banner/banner.module'; import { BannerModel } from './modules/web-information/banner/data/models/banner.model'; +import { MailModule } from './modules/configuration/mail/mail.module'; @Module({ imports: [ @@ -122,6 +123,7 @@ import { BannerModel } from './modules/web-information/banner/data/models/banner CronModule, GoogleCalendarModule, LogModule, + MailModule, MidtransModule, SessionModule, UploadModule, diff --git a/src/core/modules/domain/usecase/managers/base-update-status.manager.ts b/src/core/modules/domain/usecase/managers/base-update-status.manager.ts index f3aa9a2..3e4e0a9 100644 --- a/src/core/modules/domain/usecase/managers/base-update-status.manager.ts +++ b/src/core/modules/domain/usecase/managers/base-update-status.manager.ts @@ -9,6 +9,7 @@ export abstract class BaseUpdateStatusManager extends BaseManager { protected result: Entity; protected oldData: Entity; protected dataStatus: STATUS; + protected relations = []; protected duplicateColumn: string[]; abstract get entityTarget(): any; @@ -22,6 +23,7 @@ export abstract class BaseUpdateStatusManager extends BaseManager { where: { id: this.dataId, }, + relations: this.relations, }); this.oldData = _.cloneDeep(this.data); diff --git a/src/database/migrations/1721216510569-update-table-transactions.ts b/src/database/migrations/1721216510569-update-table-transactions.ts new file mode 100644 index 0000000..5e6380a --- /dev/null +++ b/src/database/migrations/1721216510569-update-table-transactions.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateTableTransactions1721216510569 + implements MigrationInterface +{ + name = 'UpdateTableTransactions1721216510569'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transactions" ADD "payment_midtrans_token" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "transactions" ADD "payment_midtrans_url" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "transactions" DROP COLUMN "payment_midtrans_url"`, + ); + await queryRunner.query( + `ALTER TABLE "transactions" DROP COLUMN "payment_midtrans_token"`, + ); + } +} diff --git a/src/modules/configuration/mail/domain/email-template/payment-confirmation-bank.html b/src/modules/configuration/mail/domain/email-template/payment-confirmation-bank.html new file mode 100644 index 0000000..daa023e --- /dev/null +++ b/src/modules/configuration/mail/domain/email-template/payment-confirmation-bank.html @@ -0,0 +1,413 @@ + + + + + + + Email Ibunda + + + + + + + + + + +
  +
+ + + + + + + +
+ + + + +
+

Halo {{customer_name}}

+

Pemesanan tiket telah berhasil dengan nomor invoice {{invoice_code}}, Silahkan lakukan pembayaran

+
+

+ PEMBAYARAN DAPAT MELALUI +

    + {{#each payment_methods}} +
  • +

    + {{issuer_name}}
    + Name: {{account_name}}
    + Number: {{account_number}} +

    +
  • + {{/each}} +
+

+
+
+ + + + + + +
+
 
+ + + \ No newline at end of file diff --git a/src/modules/configuration/mail/domain/email-template/payment-confirmation-midtrans.html b/src/modules/configuration/mail/domain/email-template/payment-confirmation-midtrans.html new file mode 100644 index 0000000..b1cc797 --- /dev/null +++ b/src/modules/configuration/mail/domain/email-template/payment-confirmation-midtrans.html @@ -0,0 +1,404 @@ + + + + + + + Email Ibunda + + + + + + + + + + +
  +
+ + + + + + + +
+ + + + + + + +
+

Halo {{customer_name}}

+

Pemesanan tiket telah berhasil dengan nomor invoice {{invoice_code}}, Silahkan lakukan pembayaran dengan klik button dibawah ini

+
+ Lanjutkan Pembayaran +
+
+ + + + + + +
+
 
+ + + \ No newline at end of file diff --git a/src/modules/configuration/mail/domain/handlers/payment-transaction.handler.ts b/src/modules/configuration/mail/domain/handlers/payment-transaction.handler.ts new file mode 100644 index 0000000..f050ba1 --- /dev/null +++ b/src/modules/configuration/mail/domain/handlers/payment-transaction.handler.ts @@ -0,0 +1,60 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { STATUS } from 'src/core/strings/constants/base.constants'; +import { PaymentMethodDataService } from 'src/modules/transaction/payment-method/data/services/payment-method-data.service'; +import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service'; +import { TransactionChangeStatusEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event'; +import { sendEmail } from '../helpers/send-email.helper'; +import { TransactionPaymentType } from 'src/modules/transaction/transaction/constants'; + +@EventsHandler(TransactionChangeStatusEvent) +export class PaymentTransactionHandler + implements IEventHandler +{ + constructor( + private dataService: TransactionDataService, + private paymentService: PaymentMethodDataService, + ) {} + + async handle(event: TransactionChangeStatusEvent) { + const data_id = event.data.id; + const old_data = event.data.old; + const current_data = event.data.data; + let payments = []; + + if ( + old_data.status == STATUS.DRAFT && + current_data.status == STATUS.PENDING && + current_data.payment_type != TransactionPaymentType.COUNTER + ) { + if (current_data.payment_type != TransactionPaymentType.MIDTRANS) { + payments = await this.paymentService.getManyByOptions({ + where: { + status: STATUS.ACTIVE, + }, + }); + } + + const transaction = await this.dataService.getOneByOptions({ + where: { + id: data_id, + }, + relations: ['items'], + }); + + try { + sendEmail( + [ + { + ...transaction, + email: transaction.customer_email, + payment_methods: payments, + }, + ], + 'Payment Confirmation', + ); + } catch (error) { + console.log(error); + } + } + } +} diff --git a/src/modules/configuration/mail/domain/helpers/send-email.helper.ts b/src/modules/configuration/mail/domain/helpers/send-email.helper.ts new file mode 100644 index 0000000..87dd1ee --- /dev/null +++ b/src/modules/configuration/mail/domain/helpers/send-email.helper.ts @@ -0,0 +1,50 @@ +import * as nodemailer from 'nodemailer'; +import * as handlebars from 'handlebars'; +import * as path from 'path'; +import * as fs from 'fs'; +import { TransactionPaymentType } from 'src/modules/transaction/transaction/constants'; + +export async function sendEmail(receivers, subject) { + const smtpTransport = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, + port: process.env.EMAIL_POST, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_TOKEN, + }, + }); + let templateName = 'payment-confirmation-bank'; + + for (const receiver of receivers) { + if (receiver.payment_type == TransactionPaymentType.MIDTRANS) + templateName = 'payment-confirmation-midtrans'; + + let templatePath = path.resolve( + __dirname, + `../email-template/${templateName}.html`, + ); + templatePath = templatePath.replace(/dist/g, 'src'); + const templateSource = fs.readFileSync(templatePath, 'utf8'); + + const template = handlebars.compile(templateSource); + const htmlToSend = template(receiver); + + const emailContext = { + from: 'no-reply@eigen.co.id', + to: receiver.email, + subject: subject, + html: htmlToSend, + attachDataUrls: true, + }; + + await new Promise((f) => setTimeout(f, 2000)); + + smtpTransport.sendMail(emailContext, function (err, data) { + if (err) { + console.log(`Error occurs on send to ${receiver.email}`); + } else { + console.log(`Email sent to ${receiver.email}`); + } + }); + } +} diff --git a/src/modules/configuration/mail/mail.module.ts b/src/modules/configuration/mail/mail.module.ts new file mode 100644 index 0000000..597fc9c --- /dev/null +++ b/src/modules/configuration/mail/mail.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { CqrsModule } from '@nestjs/cqrs'; +import { PaymentMethodModel } from 'src/modules/transaction/payment-method/data/models/payment-method.model'; +import { PaymentMethodDataService } from 'src/modules/transaction/payment-method/data/services/payment-method-data.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; +import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model'; +import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service'; +import { PaymentTransactionHandler } from './domain/handlers/payment-transaction.handler'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + TypeOrmModule.forFeature( + [PaymentMethodModel, TransactionModel], + CONNECTION_NAME.DEFAULT, + ), + CqrsModule, + ], + controllers: [], + providers: [ + PaymentTransactionHandler, + PaymentMethodDataService, + TransactionDataService, + ], +}) +export class MailModule {} diff --git a/yarn.lock b/yarn.lock index 382e0c6..cc75c97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5364,6 +5364,11 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +nodemailer@^6.9.14: + version "6.9.14" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.14.tgz#845fda981f9fd5ac264f4446af908a7c78027f75" + integrity sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA== + nopt@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"