Merge pull request 'feat/midtrans' (#31) from feat/midtrans into development
continuous-integration/drone/tag Build is passing
Details
continuous-integration/drone/tag Build is passing
Details
Reviewed-on: #31pull/32/head devel_10.6.20
commit
853543ece5
|
@ -22,10 +22,15 @@ CRON_MIDNIGHT="55 11 * * *"
|
||||||
CRON_EVERY_MINUTE="55 11 * * *"
|
CRON_EVERY_MINUTE="55 11 * * *"
|
||||||
CRON_EVERY_HOUR="0 * * * *"
|
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_URL=https://app.sandbox.midtrans.com
|
||||||
MIDTRANS_PRODUCTION=false
|
MIDTRANS_PRODUCTION=false
|
||||||
MIDTRANS_SERVER_KEY=SB-Mid-server-ITmSD6C0nXfIcmgi4TXm6J7i
|
MIDTRANS_SERVER_KEY=
|
||||||
MIDTRANS_CLIENT_KEY=SB-Mid-client-VFaU_cPL6kh2DKir
|
MIDTRANS_CLIENT_KEY=
|
||||||
|
|
||||||
EXPORT_LIMIT_PARTITION=200
|
EXPORT_LIMIT_PARTITION=200
|
||||||
ASSETS="https://asset.sky.eigen.co.id/"
|
ASSETS="https://asset.sky.eigen.co.id/"
|
|
@ -22,10 +22,15 @@ CRON_MIDNIGHT="55 11 * * *"
|
||||||
CRON_EVERY_MINUTE="55 11 * * *"
|
CRON_EVERY_MINUTE="55 11 * * *"
|
||||||
CRON_EVERY_HOUR="0 * * * *"
|
CRON_EVERY_HOUR="0 * * * *"
|
||||||
|
|
||||||
|
EMAIL_HOST="sandbox.smtp.mailtrap.io"
|
||||||
|
EMAIL_POST=465
|
||||||
|
EMAIL_USER=
|
||||||
|
EMAIL_TOKEN=
|
||||||
|
|
||||||
MIDTRANS_URL=https://app.midtrans.com
|
MIDTRANS_URL=https://app.midtrans.com
|
||||||
MIDTRANS_PRODUCTION=true
|
MIDTRANS_PRODUCTION=true
|
||||||
MIDTRANS_SERVER_KEY=Mid-server-6lA4Nnmov2BSOcwVq1sLSOpC
|
MIDTRANS_SERVER_KEY=
|
||||||
MIDTRANS_CLIENT_KEY=Mid-client-JiPIkIPd_RGooF8U
|
MIDTRANS_CLIENT_KEY=
|
||||||
|
|
||||||
EXPORT_LIMIT_PARTITION=200
|
EXPORT_LIMIT_PARTITION=200
|
||||||
ASSETS="https://asset.sky.eigen.co.id/"
|
ASSETS="https://asset.sky.eigen.co.id/"
|
|
@ -46,10 +46,12 @@
|
||||||
"elastic-apm-node": "^4.5.4",
|
"elastic-apm-node": "^4.5.4",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"googleapis": "^140.0.0",
|
"googleapis": "^140.0.0",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
"mathjs": "^13.0.2",
|
"mathjs": "^13.0.2",
|
||||||
"midtrans-client": "^1.3.1",
|
"midtrans-client": "^1.3.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nano": "^10.1.3",
|
"nano": "^10.1.3",
|
||||||
|
"nodemailer": "^6.9.14",
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"plop": "^4.0.1",
|
"plop": "^4.0.1",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
|
|
|
@ -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 { NewsModel } from './modules/web-information/news/data/models/news.model';
|
||||||
import { BannerModule } from './modules/web-information/banner/banner.module';
|
import { BannerModule } from './modules/web-information/banner/banner.module';
|
||||||
import { BannerModel } from './modules/web-information/banner/data/models/banner.model';
|
import { BannerModel } from './modules/web-information/banner/data/models/banner.model';
|
||||||
|
import { MailModule } from './modules/configuration/mail/mail.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -122,6 +123,7 @@ import { BannerModel } from './modules/web-information/banner/data/models/banner
|
||||||
CronModule,
|
CronModule,
|
||||||
GoogleCalendarModule,
|
GoogleCalendarModule,
|
||||||
LogModule,
|
LogModule,
|
||||||
|
MailModule,
|
||||||
MidtransModule,
|
MidtransModule,
|
||||||
SessionModule,
|
SessionModule,
|
||||||
UploadModule,
|
UploadModule,
|
||||||
|
|
|
@ -9,6 +9,7 @@ export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
|
||||||
protected result: Entity;
|
protected result: Entity;
|
||||||
protected oldData: Entity;
|
protected oldData: Entity;
|
||||||
protected dataStatus: STATUS;
|
protected dataStatus: STATUS;
|
||||||
|
protected relations = [];
|
||||||
protected duplicateColumn: string[];
|
protected duplicateColumn: string[];
|
||||||
abstract get entityTarget(): any;
|
abstract get entityTarget(): any;
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
|
||||||
where: {
|
where: {
|
||||||
id: this.dataId,
|
id: this.dataId,
|
||||||
},
|
},
|
||||||
|
relations: this.relations,
|
||||||
});
|
});
|
||||||
this.oldData = _.cloneDeep(this.data);
|
this.oldData = _.cloneDeep(this.data);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class UpdateTableTransactions1721216510569
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'UpdateTableTransactions1721216510569';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "transactions" DROP COLUMN "payment_midtrans_url"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "transactions" DROP COLUMN "payment_midtrans_token"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,413 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Email Ibunda</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
|
||||||
|
down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn>tbody>tr>td {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
padding: 0 0 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol li {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
<table class="main">
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Halo {{customer_name}}</p>
|
||||||
|
<p>Pemesanan tiket telah berhasil dengan nomor invoice {{invoice_code}}, Silahkan lakukan pembayaran</p>
|
||||||
|
<br>
|
||||||
|
<p>
|
||||||
|
<b>PEMBAYARAN DAPAT MELALUI</b>
|
||||||
|
<ul>
|
||||||
|
{{#each payment_methods}}
|
||||||
|
<li>
|
||||||
|
<p>
|
||||||
|
<b>{{issuer_name}}</b><br>
|
||||||
|
<span>Name: <b>{{account_name}}</b></span><br>
|
||||||
|
<span>Number: <b>{{account_number}}</b></span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by">
|
||||||
|
Powered by Skyworld
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,404 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>Email Ibunda</title>
|
||||||
|
<style>
|
||||||
|
/* -------------------------------------
|
||||||
|
GLOBAL RESETS
|
||||||
|
------------------------------------- */
|
||||||
|
img {
|
||||||
|
border: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
font-family: sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BODY & CONTAINER
|
||||||
|
------------------------------------- */
|
||||||
|
|
||||||
|
.body {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
|
||||||
|
down on a phone or something */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto !important;
|
||||||
|
/* makes it centered */
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 580px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||||
|
.content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
Margin: 0 auto;
|
||||||
|
max-width: 580px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
HEADER, FOOTER, MAIN
|
||||||
|
------------------------------------- */
|
||||||
|
.main {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
clear: both;
|
||||||
|
Margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer td,
|
||||||
|
.footer p,
|
||||||
|
.footer span,
|
||||||
|
.footer a {
|
||||||
|
color: #999999;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
TYPOGRAPHY
|
||||||
|
------------------------------------- */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
color: #000000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 35px;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
Margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p li,
|
||||||
|
ul li,
|
||||||
|
ol li {
|
||||||
|
list-style-position: inside;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
BUTTONS
|
||||||
|
------------------------------------- */
|
||||||
|
.btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn>tbody>tr>td {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn table td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn a {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: solid 1px #3498db;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 25px;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td {
|
||||||
|
background-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a {
|
||||||
|
background-color: #3498db;
|
||||||
|
border-color: #3498db;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
OTHER STYLES THAT MIGHT BE USEFUL
|
||||||
|
------------------------------------- */
|
||||||
|
.last {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt0 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
color: transparent;
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.powered-by a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
Margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||||
|
------------------------------------- */
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------
|
||||||
|
PRESERVE THESE STYLES IN THE HEAD
|
||||||
|
------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
padding: 0 0 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol li {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||||
|
<tr>
|
||||||
|
<td> </td>
|
||||||
|
<td class="container">
|
||||||
|
<div class="content">
|
||||||
|
<table class="main">
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p>Halo {{customer_name}}</p>
|
||||||
|
<p>Pemesanan tiket telah berhasil dengan nomor invoice {{invoice_code}}, Silahkan lakukan pembayaran dengan klik button dibawah ini</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"
|
||||||
|
style="font-family: 'Lato', sans-serif; font-size:22px; color:#e5eaf5; line-height:24px; font-weight: 600;">
|
||||||
|
<a href="{{payment_midtrans_url}}">Lanjutkan Pembayaran</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by">
|
||||||
|
Powered by Skyworld
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -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<TransactionChangeStatusEvent>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {}
|
|
@ -1,5 +1,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { EventBus } from '@nestjs/cqrs';
|
import { EventBus } from '@nestjs/cqrs';
|
||||||
|
import { mappingMidtransTransaction } from '../../domain/usecases/helpers/mapping-transaction.helper';
|
||||||
const midtransClient = require('midtrans-client');
|
const midtransClient = require('midtrans-client');
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -19,20 +20,7 @@ export class MidtransService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(body): Promise<any> {
|
async create(body): Promise<any> {
|
||||||
return await this.midtransInstance.createTransaction({
|
const data = mappingMidtransTransaction(body);
|
||||||
transaction_details: {
|
return await this.midtransInstance.createTransaction(data);
|
||||||
order_id: '123',
|
|
||||||
gross_amount: 10000,
|
|
||||||
},
|
|
||||||
credit_card: {
|
|
||||||
secure: true,
|
|
||||||
},
|
|
||||||
customer_details: {
|
|
||||||
first_name: 'budd',
|
|
||||||
last_name: 'pratama',
|
|
||||||
email: 'budi.pra@mail.com',
|
|
||||||
phone: '0822111111',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
export function mappingMidtransTransaction(transaction) {
|
||||||
|
const item_details = transaction.items?.map((item) => {
|
||||||
|
return {
|
||||||
|
quantity: Number(item.qty),
|
||||||
|
price: Number(item.total_price),
|
||||||
|
name: item.item_name,
|
||||||
|
category: item.item_category_name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (transaction.payment_discount_total) {
|
||||||
|
item_details.push({
|
||||||
|
quantity: 1,
|
||||||
|
price: -Number(transaction.payment_discount_total),
|
||||||
|
name: 'discount',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
transaction_details: {
|
||||||
|
order_id: transaction.id,
|
||||||
|
gross_amount: Number(transaction.payment_total),
|
||||||
|
},
|
||||||
|
item_details: item_details,
|
||||||
|
customer_details: {
|
||||||
|
first_name: transaction.customer_name,
|
||||||
|
email: transaction.customer_email,
|
||||||
|
phone: transaction.customer_phone,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,19 +2,16 @@ import { ConfigModule } from '@nestjs/config';
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import { MidtransController } from './infrastructure/midtrans.controller';
|
import { MidtransController } from './infrastructure/midtrans.controller';
|
||||||
import { MidtransService } from './data/services/midtrans.service';
|
import { MidtransService } from './data/services/midtrans.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
// TypeOrmModule.forFeature(
|
|
||||||
// [
|
|
||||||
// ],
|
|
||||||
// CONNECTION_NAME.DEFAULT,
|
|
||||||
// ),
|
|
||||||
CqrsModule,
|
CqrsModule,
|
||||||
],
|
],
|
||||||
controllers: [MidtransController],
|
controllers: [MidtransController],
|
||||||
providers: [MidtransService],
|
providers: [MidtransService],
|
||||||
|
exports: [MidtransService],
|
||||||
})
|
})
|
||||||
export class MidtransModule {}
|
export class MidtransModule {}
|
||||||
|
|
|
@ -125,6 +125,12 @@ export class TransactionModel
|
||||||
@Column('varchar', { name: 'payment_code_reference', nullable: true })
|
@Column('varchar', { name: 'payment_code_reference', nullable: true })
|
||||||
payment_code_reference: string;
|
payment_code_reference: string;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'payment_midtrans_token', nullable: true })
|
||||||
|
payment_midtrans_token: string;
|
||||||
|
|
||||||
|
@Column('varchar', { name: 'payment_midtrans_url', nullable: true })
|
||||||
|
payment_midtrans_url: string;
|
||||||
|
|
||||||
@Column('date', { name: 'payment_date', nullable: true })
|
@Column('date', { name: 'payment_date', nullable: true })
|
||||||
payment_date: Date;
|
payment_date: Date;
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,8 @@ export interface TransactionEntity extends BaseStatusEntity {
|
||||||
payment_type_method_qr: string;
|
payment_type_method_qr: string;
|
||||||
payment_card_information: string;
|
payment_card_information: string;
|
||||||
payment_code_reference: string;
|
payment_code_reference: string;
|
||||||
|
payment_midtrans_token: string;
|
||||||
|
payment_midtrans_url: string;
|
||||||
payment_date: Date;
|
payment_date: Date;
|
||||||
|
|
||||||
// calculation data
|
// calculation data
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
|
||||||
|
import { MidtransCallbackEvent } from 'src/modules/configuration/midtrans/domain/entities/midtrans-callback.event';
|
||||||
|
import { TransactionDataService } from '../../../data/services/transaction-data.service';
|
||||||
|
import { TransactionModel } from '../../../data/models/transaction.model';
|
||||||
|
import { STATUS } from 'src/core/strings/constants/base.constants';
|
||||||
|
|
||||||
|
@EventsHandler(MidtransCallbackEvent)
|
||||||
|
export class MidtransCallbackHandler
|
||||||
|
implements IEventHandler<MidtransCallbackEvent>
|
||||||
|
{
|
||||||
|
constructor(private dataService: TransactionDataService) {}
|
||||||
|
|
||||||
|
async handle(event: MidtransCallbackEvent) {
|
||||||
|
const data_id = event.data.id;
|
||||||
|
const data = event.data.data;
|
||||||
|
const transaction = await this.dataService.getOneByOptions({
|
||||||
|
where: {
|
||||||
|
id: data_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (['capture', 'settlement'].includes(data.transaction_status)) {
|
||||||
|
Object.assign(transaction, {
|
||||||
|
status: STATUS.SETTLED,
|
||||||
|
settlement_date: new Date(data.settlement_time),
|
||||||
|
payment_code_reference: data.approval_code,
|
||||||
|
});
|
||||||
|
} else if (['pending'].includes(data['transaction_status'])) {
|
||||||
|
Object.assign(transaction, {
|
||||||
|
status: STATUS.PENDING,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.assign(transaction, {
|
||||||
|
status: STATUS.EXPIRED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryRunner = this.dataService
|
||||||
|
.getRepository()
|
||||||
|
.manager.connection.createQueryRunner();
|
||||||
|
|
||||||
|
await this.dataService.create(queryRunner, TransactionModel, transaction);
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,10 +30,12 @@ export class BatchConfirmDataTransactionManager extends BaseBatchUpdateStatusMan
|
||||||
|
|
||||||
Object.assign(data, {
|
Object.assign(data, {
|
||||||
status: STATUS.WAITING,
|
status: STATUS.WAITING,
|
||||||
reconciliation_status:
|
reconciliation_status: [
|
||||||
data.payment_type == TransactionPaymentType.COUNTER
|
TransactionPaymentType.COUNTER,
|
||||||
? null
|
TransactionPaymentType.MIDTRANS,
|
||||||
: STATUS.PENDING,
|
].includes(data.payment_type)
|
||||||
|
? null
|
||||||
|
: STATUS.PENDING,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -13,9 +13,13 @@ import {
|
||||||
UnprocessableEntityException,
|
UnprocessableEntityException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { STATUS } from 'src/core/strings/constants/base.constants';
|
import { STATUS } from 'src/core/strings/constants/base.constants';
|
||||||
|
import { generateInvoiceCodeHelper } from './helpers/generate-invoice-code.helper';
|
||||||
|
import { TransactionPaymentType } from '../../../constants';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BatchConfirmTransactionManager extends BaseBatchUpdateStatusManager<TransactionEntity> {
|
export class BatchConfirmTransactionManager extends BaseBatchUpdateStatusManager<TransactionEntity> {
|
||||||
|
protected relations = ['items'];
|
||||||
|
|
||||||
async validateData(data: TransactionEntity): Promise<void> {
|
async validateData(data: TransactionEntity): Promise<void> {
|
||||||
if (data.status != STATUS.DRAFT) {
|
if (data.status != STATUS.DRAFT) {
|
||||||
throw new UnprocessableEntityException({
|
throw new UnprocessableEntityException({
|
||||||
|
@ -26,7 +30,26 @@ export class BatchConfirmTransactionManager extends BaseBatchUpdateStatusManager
|
||||||
}
|
}
|
||||||
|
|
||||||
const freeTransaction = data.payment_total < 1;
|
const freeTransaction = data.payment_total < 1;
|
||||||
|
if (data.payment_type == TransactionPaymentType.MIDTRANS) {
|
||||||
|
try {
|
||||||
|
const { token, redirect_url } = await this.dataServiceFirstOpt.create(
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
Object.assign(data, {
|
||||||
|
payment_midtrans_token: token,
|
||||||
|
payment_midtrans_url: redirect_url,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnprocessableEntityException({
|
||||||
|
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
|
message: `Failed! this transaction already created, please check your email to continue payment`,
|
||||||
|
error: 'Unprocessable Entity',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(data, {
|
Object.assign(data, {
|
||||||
|
invoice_code: await generateInvoiceCodeHelper(this.dataService),
|
||||||
status: freeTransaction ? STATUS.ACTIVE : STATUS.PENDING,
|
status: freeTransaction ? STATUS.ACTIVE : STATUS.PENDING,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -39,10 +39,12 @@ export class ConfirmDataTransactionManager extends BaseUpdateStatusManager<Trans
|
||||||
|
|
||||||
Object.assign(this.data, {
|
Object.assign(this.data, {
|
||||||
status: STATUS.WAITING,
|
status: STATUS.WAITING,
|
||||||
reconciliation_status:
|
reconciliation_status: [
|
||||||
this.oldData.payment_type == TransactionPaymentType.COUNTER
|
TransactionPaymentType.COUNTER,
|
||||||
? null
|
TransactionPaymentType.MIDTRANS,
|
||||||
: STATUS.PENDING,
|
].includes(this.oldData.payment_type)
|
||||||
|
? null
|
||||||
|
: STATUS.PENDING,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,12 @@ import { TransactionModel } from '../../../data/models/transaction.model';
|
||||||
import { TransactionChangeStatusEvent } from '../../entities/event/transaction-change-status.event';
|
import { TransactionChangeStatusEvent } from '../../entities/event/transaction-change-status.event';
|
||||||
import { STATUS } from 'src/core/strings/constants/base.constants';
|
import { STATUS } from 'src/core/strings/constants/base.constants';
|
||||||
import { generateInvoiceCodeHelper } from './helpers/generate-invoice-code.helper';
|
import { generateInvoiceCodeHelper } from './helpers/generate-invoice-code.helper';
|
||||||
|
import { TransactionPaymentType } from '../../../constants';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConfirmTransactionManager extends BaseUpdateStatusManager<TransactionEntity> {
|
export class ConfirmTransactionManager extends BaseUpdateStatusManager<TransactionEntity> {
|
||||||
|
protected relations = ['items'];
|
||||||
|
|
||||||
getResult(): string {
|
getResult(): string {
|
||||||
return `Success active data ${this.result.invoice_code}`;
|
return `Success active data ${this.result.invoice_code}`;
|
||||||
}
|
}
|
||||||
|
@ -33,6 +36,25 @@ export class ConfirmTransactionManager extends BaseUpdateStatusManager<Transacti
|
||||||
|
|
||||||
async beforeProcess(): Promise<void> {
|
async beforeProcess(): Promise<void> {
|
||||||
const freeTransaction = this.data.payment_total < 1;
|
const freeTransaction = this.data.payment_total < 1;
|
||||||
|
|
||||||
|
if (this.data.payment_type == TransactionPaymentType.MIDTRANS) {
|
||||||
|
try {
|
||||||
|
const { token, redirect_url } = await this.dataServiceFirstOpt.create(
|
||||||
|
this.oldData,
|
||||||
|
);
|
||||||
|
Object.assign(this.data, {
|
||||||
|
payment_midtrans_token: token,
|
||||||
|
payment_midtrans_url: redirect_url,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnprocessableEntityException({
|
||||||
|
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
|
message: `Failed! this transaction already created, please check your email to continue payment`,
|
||||||
|
error: 'Unprocessable Entity',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this.data, {
|
Object.assign(this.data, {
|
||||||
invoice_code: await generateInvoiceCodeHelper(this.dataService),
|
invoice_code: await generateInvoiceCodeHelper(this.dataService),
|
||||||
status: freeTransaction ? STATUS.ACTIVE : STATUS.PENDING,
|
status: freeTransaction ? STATUS.ACTIVE : STATUS.PENDING,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { CancelTransactionManager } from './managers/cancel-transaction.manager'
|
||||||
import { BatchCancelTransactionManager } from './managers/batch-cancel-transaction.manager';
|
import { BatchCancelTransactionManager } from './managers/batch-cancel-transaction.manager';
|
||||||
import { ConfirmDataTransactionManager } from './managers/confirm-data-transaction.manager';
|
import { ConfirmDataTransactionManager } from './managers/confirm-data-transaction.manager';
|
||||||
import { BatchConfirmDataTransactionManager } from './managers/batch-confirm-data-transaction.manager';
|
import { BatchConfirmDataTransactionManager } from './managers/batch-confirm-data-transaction.manager';
|
||||||
|
import { MidtransService } from 'src/modules/configuration/midtrans/data/services/midtrans.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TransactionDataOrchestrator {
|
export class TransactionDataOrchestrator {
|
||||||
|
@ -29,6 +30,7 @@ export class TransactionDataOrchestrator {
|
||||||
private cancelManager: CancelTransactionManager,
|
private cancelManager: CancelTransactionManager,
|
||||||
private batchCancelManager: BatchCancelTransactionManager,
|
private batchCancelManager: BatchCancelTransactionManager,
|
||||||
private serviceData: TransactionDataService,
|
private serviceData: TransactionDataService,
|
||||||
|
private midtransService: MidtransService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(data): Promise<TransactionEntity> {
|
async create(data): Promise<TransactionEntity> {
|
||||||
|
@ -81,7 +83,11 @@ export class TransactionDataOrchestrator {
|
||||||
|
|
||||||
async confirm(dataId): Promise<string> {
|
async confirm(dataId): Promise<string> {
|
||||||
this.confirmManager.setData(dataId, STATUS.ACTIVE);
|
this.confirmManager.setData(dataId, STATUS.ACTIVE);
|
||||||
this.confirmManager.setService(this.serviceData, TABLE_NAME.TRANSACTION);
|
this.confirmManager.setService(
|
||||||
|
this.serviceData,
|
||||||
|
TABLE_NAME.TRANSACTION,
|
||||||
|
this.midtransService,
|
||||||
|
);
|
||||||
await this.confirmManager.execute();
|
await this.confirmManager.execute();
|
||||||
return this.confirmManager.getResult();
|
return this.confirmManager.getResult();
|
||||||
}
|
}
|
||||||
|
@ -91,6 +97,7 @@ export class TransactionDataOrchestrator {
|
||||||
this.batchConfirmManager.setService(
|
this.batchConfirmManager.setService(
|
||||||
this.serviceData,
|
this.serviceData,
|
||||||
TABLE_NAME.TRANSACTION,
|
TABLE_NAME.TRANSACTION,
|
||||||
|
this.midtransService,
|
||||||
);
|
);
|
||||||
await this.batchConfirmManager.execute();
|
await this.batchConfirmManager.execute();
|
||||||
return this.batchConfirmManager.getResult();
|
return this.batchConfirmManager.getResult();
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { SalesPriceFormulaModel } from '../sales-price-formula/data/models/sales
|
||||||
import { TaxModel } from '../tax/data/models/tax.model';
|
import { TaxModel } from '../tax/data/models/tax.model';
|
||||||
import { SettledTransactionHandler } from './domain/usecases/handlers/settled-transaction.handler';
|
import { SettledTransactionHandler } from './domain/usecases/handlers/settled-transaction.handler';
|
||||||
import { RefundUpdatedHandler } from './domain/usecases/handlers/refund-update.handler';
|
import { RefundUpdatedHandler } from './domain/usecases/handlers/refund-update.handler';
|
||||||
|
import { MidtransCallbackHandler } from './domain/usecases/handlers/midtrans-transaction-callback.handler';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -51,6 +52,7 @@ import { RefundUpdatedHandler } from './domain/usecases/handlers/refund-update.h
|
||||||
providers: [
|
providers: [
|
||||||
RefundUpdatedHandler,
|
RefundUpdatedHandler,
|
||||||
PosTransactionHandler,
|
PosTransactionHandler,
|
||||||
|
MidtransCallbackHandler,
|
||||||
SettledTransactionHandler,
|
SettledTransactionHandler,
|
||||||
|
|
||||||
IndexTransactionManager,
|
IndexTransactionManager,
|
||||||
|
|
|
@ -5364,6 +5364,11 @@ node-releases@^2.0.14:
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
|
||||||
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
|
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:
|
nopt@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
|
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
|
||||||
|
|
Loading…
Reference in New Issue