Merge branch 'development' into staging
commit
afa63667ab
53
.drone.yml
53
.drone.yml
|
@ -1,38 +1,35 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: build
|
||||
name: server
|
||||
steps:
|
||||
- name: build-dev
|
||||
image: plugins/docker
|
||||
- name: build
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
registry: registry.eigen.co.id
|
||||
repo: registry.eigen.co.id/eigen/${DRONE_REPO_NAME}
|
||||
build_args:
|
||||
- env_target=env.development
|
||||
tags: latest
|
||||
custom_dns: 172.10.10.16
|
||||
host:
|
||||
- 172.10.10.10
|
||||
username: eigen
|
||||
key:
|
||||
from_secret: DEVOPS_SSH_PRIVATE_OPEN
|
||||
port: 22
|
||||
script:
|
||||
- cd /home/eigen/PROJECT/POS/POS.DEV/BE
|
||||
- sh build.sh
|
||||
- name: send-message
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls: https://mattermost.eigen.co.id/api/v4/posts
|
||||
content_type: application/json
|
||||
headers:
|
||||
- Authorization=Bearer 5zubexudb38uuradfa36qy98ca
|
||||
template: |
|
||||
{
|
||||
"channel_id": "s1ekqde1c3du5p35g6budnuotc",
|
||||
"message": "Build {{repo.name}} sudah selesai"
|
||||
}
|
||||
trigger:
|
||||
ref:
|
||||
- refs/tags/devel_*
|
||||
- refs/tags/*-alpha.*
|
||||
event:
|
||||
exclude:
|
||||
- promote
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: deployment
|
||||
steps:
|
||||
- name: deployment
|
||||
image: alpine
|
||||
failure: ignore
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- curl -X POST https://manager.sky.eigen.co.id/api/webhooks/806de7e2-1d3e-4889-b472-a59af0a5eb33
|
||||
trigger:
|
||||
ref:
|
||||
- refs/tags/devel_*
|
||||
event:
|
||||
exclude:
|
||||
- promote
|
||||
depends_on:
|
||||
- build
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: build
|
||||
steps:
|
||||
- name: build-dev
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: registry.eigen.co.id
|
||||
repo: registry.eigen.co.id/eigen/${DRONE_REPO_NAME}
|
||||
build_args:
|
||||
- env_target=env.development
|
||||
tags: latest
|
||||
custom_dns: 172.10.10.16
|
||||
trigger:
|
||||
ref:
|
||||
- refs/tags/devel_*
|
||||
event:
|
||||
exclude:
|
||||
- promote
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: deployment
|
||||
steps:
|
||||
- name: deployment
|
||||
image: alpine
|
||||
failure: ignore
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- curl -X POST https://manager.sky.eigen.co.id/api/webhooks/806de7e2-1d3e-4889-b472-a59af0a5eb33
|
||||
trigger:
|
||||
ref:
|
||||
- refs/tags/devel_*
|
||||
event:
|
||||
exclude:
|
||||
- promote
|
||||
depends_on:
|
||||
- build
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Nest Framework",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
|
||||
"autoAttachChildProcesses": true,
|
||||
"restart": true,
|
||||
"sourceMaps": true,
|
||||
"stopOnEntry": false,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -9,5 +9,6 @@ FROM node:18.17-alpine
|
|||
WORKDIR /app
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/assets ./assets
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
CMD ["node", "--max-old-space-size=8192","--max-http-header-size", "512000", "-r", "dotenv/config", "dist/main"]
|
|
@ -4,7 +4,7 @@
|
|||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Ibunda</title>
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
|
@ -364,45 +364,36 @@
|
|||
<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>
|
||||
<p class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p class="mb0">Great News! We've successfully updated your booking date as per your request.</p>
|
||||
<p>We're excited to accommodate your new plans and ensure evertyhing goes smoothly.</p>
|
||||
|
||||
<p class="mb0">Here are your updated booking details</p>
|
||||
<b class="mb0">Original Booking Date: {{booking_date_before}}</b>
|
||||
<b>New Booking Date: {{booking_date}}</b>
|
||||
|
||||
<p class="mb0">For yout convenience, we've attached a new confirmation receipt reflecting these changes.</p>
|
||||
<p><b>Please be sure to bring this updated receipt with you on the new date</b></p>
|
||||
|
||||
<p class="mb0">To keep the good times rolling, our friendly support team is just a call away at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
<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>
|
||||
|
||||
<p>Thank you and we can't wait to see you and make sure you have an amazing time!</p>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</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>
|
|
@ -0,0 +1,412 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</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 class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p>Thank you fot choosing us! We're absolutelty thrilled and can't wait to embark on this exciting day with you. See you soon for fun times ahead</p>
|
||||
|
||||
<p class="mb0">Here's a quick recap of your invoice:</p>
|
||||
<p class="mb0">Booking Date: {{booking_date}}</p>
|
||||
<p>Total Invoice: {{payment_total}}</p>
|
||||
|
||||
<p class="mb0">Just a friendly reminder that your invoice will expire on {{expire_date}}</p>
|
||||
<p>To keep things running smoothly, please ensure your payment is completed before this data</p>
|
||||
|
||||
<p>
|
||||
For your convenience, here is a list of our account details:
|
||||
<ul>
|
||||
{{#each payment_methods}}
|
||||
<li>
|
||||
<p>{{issuer_name}} {{account_number}} a/n >{{account_name}}</p>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p class="mb0">Once you've made the payment, please kindly email or send the proof of payment so we can proceed with your booking promptly</p>
|
||||
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -4,7 +4,7 @@
|
|||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Ibunda</title>
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
|
@ -364,36 +364,32 @@
|
|||
<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>
|
||||
<p class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p>We hope this message finds you well!</p>
|
||||
|
||||
<p class="mb0">Uh-oh! it looks like your invoice, dated {{invoice_date}}, has officially expired as of {{expired_date}}</p>
|
||||
<p>But no worries, we can fix this together!</p>
|
||||
|
||||
<p class="mb0">To keep the good times rolling, our friendly support team is just a call away at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
|
||||
<p class="mb0">Here are the details of the expired invoice:</p>
|
||||
<p class="mb0">Booking Date: {{booking_date}}</p>
|
||||
<p>Total Invoice: {{total_payment}}</p>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</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>
|
|
@ -0,0 +1,419 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</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 class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p>Thank you fot choosing us! We're absolutelty thrilled and can't wait to embark on this exciting day with you. See you soon for fun times ahead</p>
|
||||
|
||||
<p class="mb0">Here's a quick recap of your invoice:</p>
|
||||
<p class="mb0">Booking Date: {{booking_date}}</p>
|
||||
<p>Total Invoice: {{payment_total}}</p>
|
||||
|
||||
<p class="mb0">Just a friendly reminder that your invoice will expire on {{expire_date}}</p>
|
||||
<p>To keep things running smoothly, please ensure your payment is completed before this data</p>
|
||||
|
||||
<p class="mb0">For your convenience, here is a list of our account details:</p>
|
||||
<a href="{{payment_midtrans_url}}">{{payment_midtrans_url}}</a>
|
||||
<br><br>
|
||||
|
||||
<p class="mb0">Once you've made the payment, please kindly email or send the proof of payment so we can proceed with your booking promptly</p>
|
||||
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</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,412 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</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 class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p class="mb0">We are excited to inform you that your payment has been successfully received!</p>
|
||||
<p class="mb0">Attached to this email, you will find your confirmatin receipt</p>
|
||||
<p class="mb0">Please keep this safe as you will need to show it at the entrance upon your arrival</p>
|
||||
<p>It's your golden ticket to all the fun and excitement awaiting you!</p>
|
||||
|
||||
<br>
|
||||
<p class="mb0">Here's a quick recap:</p>
|
||||
|
||||
<p class="mb0">Booking Date: {{booking_date}}</p>
|
||||
<p class="mb0">Invoice Code: {{invoice_code}}</p>
|
||||
<p class="mb0">Payment Date: {{payment_date}}</p>
|
||||
<p class="mb0">Payment Code: {{payment_code}}</p>
|
||||
<p class="mb0">Payment Via: {{payment_via}}</p>
|
||||
<p class="mb0">Account No: {{account_no}}</p>
|
||||
<p>On Behalf Of: {{account_name}}</p>
|
||||
<br>
|
||||
|
||||
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
<br>
|
||||
|
||||
<p class="mb0">Font forget to bring a smile and your confirmation receipt (attached) for a smooth entry</p>
|
||||
<p>We can't wait to see you and ensure you have an amazing time with us!</p>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,415 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</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 class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p class="mb-0">Good News!</p>
|
||||
<p>We've successfully processed your refund for:</p>
|
||||
|
||||
<p class="mb0">Here are the details of your refund:</p>
|
||||
<p class="mb0">Transaction Date: <b>{{booking_date}}</b></p>
|
||||
<p class="mb0">Transaction Code: <b>{{invoice_code}}</b></p>
|
||||
<p class="mb0">Total Refund: <b>{{refund.refund_total}}</b></p>
|
||||
<p>{{{refund_items}}}</p>
|
||||
|
||||
<p class="mb0">Transaction Number: <b>{{invoice_code}}</b></p>
|
||||
<p class="mb0">Refund Processed Date: <b>{{refund.refund_date}}</b></p>
|
||||
<p class="mb0">Bank Account: <b>{{refund.bank_name}}</b></p>
|
||||
<p class="mb0">Account Number: <b>{{refund.bank_account_number}}</b></p>
|
||||
<p>Account Name: <b>{{refund.bank_account_name}}</b></p>
|
||||
|
||||
<p class="mb0">We hope this helps make things right, and we're here to assist if you need anything else</p>
|
||||
<p>You should see the refund in your account within 3 business days</p>
|
||||
|
||||
|
||||
<p class="mb0">Thank you for your patience and understanding</p>
|
||||
<p>If you have any questions or need further assistance, don't hesitate to reach out us at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
|
||||
<br>
|
||||
|
||||
<p class="mb0">Thank you and we can't wait to see you and make sure you have an amazing time!</p>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,409 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</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 class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p class="mb0">We're trully sorry for any inconvenience that led to this request</p>
|
||||
<p>We've received your refund request for :</p>
|
||||
|
||||
<p class="mb0">Transaction Date: <b>{{booking_date}}</b></p>
|
||||
<p class="mb0">Transaction Code: <b>{{invoice_code}}</b></p>
|
||||
<p class="mb0">Refund Code: <b>{{refund.code}}</b></p>
|
||||
<p class="mb0">Total Refund: <b>{{refund.refund_total}}</b></p>
|
||||
<p>{{{refund_items}}}</p>
|
||||
|
||||
<p class="mb0">Your satisfaction is important to us, and we're commited to resolving this as quickly as possible</p>
|
||||
<p class="mb0">Our team is already on it and will process your refund request promptly</p>
|
||||
<p>We'll keep you updated and notify you once the refund has been processed</p>
|
||||
|
||||
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
|
||||
<br>
|
||||
|
||||
<p class="mb0">Thank you for your patience and understanding</p>
|
||||
<p>We appriciate your feedback and are here to make things right</p>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"type": "service_account",
|
||||
"project_id": "weplayground-app",
|
||||
"private_key_id": "e3ed1a4430140ac589c6e9e7ce125d16d8f7304a",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCmEl90K7ojdx1\nnJv5BKq3THI+l+pgC4dlqOuEV4sc2SFXECgyEgEYAFH6U8eH9TTl5wW5hFhvpiWl\nHSxZA2nMa1ojp97mkufzaGgsJbcB4ni9ydoJyN9Hqs2Wz+JiBtjscGOrmOP1bNyn\nBO9RhHInh0bfTNMrtsIicr4DPNIfM2sl95v6pCDGt7Cfu2NoEnDhId50d73KVONI\n0+rf90WhehEMwoZEzYI0gLmSVbnPEm1j4/OOQfQl7FjaFKyle+A5BWaiRsIqiSue\n0jvZz0DlGmjeHx1yjBIKpq5omOku7aYi4kTNEZKKxzs5HhRFKi2KuYNK/WD5ApQg\ncIhGhhCfAgMBAAECggEANX+LmNjh9VJm/Tigkt4LFxifwgCe8WfKAhNmKHyu5K/3\nIAnzmwxjG5ee8gzNat3pfJk+dCnj7FIHwHScSB6NnCMZZXsV51sVBNC77wMxZIXA\nPyE63fzJEdlt6xvc96k9QweFB1yhs0wJ/6r2JnmcrqxcujBTUA3PIoxcG+TBOc08\ndo5Rcbeq6/3txjGlFM1820WViuFSQQiL6PgNVb+l0JrQ8rAOflKYFOkUb8wux9LX\nnD4vJMwa0j+GRvH5BCcZCguIQZn2JR3rTgcavWtcaHiTNsc49Lsj/hGGOsbkFROo\nGWaSgXE169xiVR/MMEblzqpSXq1qXF2iUeaqyUFIZQKBgQDxxrNlDs1qMfcaQ0S2\nVVtU/f1NfY+kCjQaC4CoYJaaoZINs5ODPs8/2DGnHuhNXMtnPeQ+SzNaK1e1eLbw\nmvq1+n3aGZTvUq2L3b+v7JJ6TQmQ4eBLZBzNjxrxC3EkCULTuROtsAhfzORuE0mE\nwnhR5LpPraEBrPi0re9yDDXVHQKBgQDOCwGw1gNVLh622qR65Zhx5rs2q6ktPxq2\neiUV0KDug6/7QbJzg1pNeoVQmadJR86H0fzKMsN5C7t7z3MIkqXc0+T1NmdN2fPm\ndLthnR1grCDYykoet/CITbAfiip27/o3TJ7YIYItefyZ4GnNH82R/4z3LBDnXB9f\n565hbUj76wKBgEnNMpOFijSBXgFZSU8zDPcLtNeDnWYgazkMC9DZ8v7ulOuzxjKI\n6LB/aOCvsY9z5O712IcfY2SB2HsfhxA47pDADsyVhH3tSeZo4QttdmT4wRPFrza0\nL4qbxUiRCo9KeGiylQwusM+1doEXSBjLV/j/jdOml4AwcZaNhYrVqVUNAoGAU0uD\nzXdXNZJFfGp7X+t9a155hKp05APEyswqPd1vkbzO4eY3PBd35CaJyoGzbR6IUcQE\nS8Gl4ENr8at1t5uBTfqjbrYloQVhYmMCdX3MqI4tYTa2LCD0LkYp0zZJ4Hc3Ui+5\nb2psc/ICujpMy032DvWeiTXZR46oaF8C0gQaIy0CgYEAmKCP4CXmPlWoWqebFp3W\nz2eKWUfASioQ+ZGUVNEge4a6iutciydQJZxBfg9ZXWqDfI0FoRSPfs2zUZFO0AcM\n6oaPGiFnTnH8FGcSHu3p0YysevyoSY6tgsAhb3IiKjJd4e7btsYzpPZbIfyfUVHK\nQFOOSkE+x4J5ts+XO6isQ+w=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "weplayground@weplayground-app.iam.gserviceaccount.com",
|
||||
"client_id": "106351339097550564510",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/weplayground%40weplayground-app.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
|
@ -22,15 +22,21 @@ CRON_MIDNIGHT="55 11 * * *"
|
|||
CRON_EVERY_MINUTE="55 11 * * *"
|
||||
CRON_EVERY_HOUR="0 * * * *"
|
||||
|
||||
EMAIL_HOST="sandbox.smtp.mailtrap.io"
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_POST=465
|
||||
EMAIL_USER="developer@eigen.co.id"
|
||||
EMAIL_TOKEN="bitqkbkzjzfywxqx"
|
||||
EMAIL_USER=weplayground.app@gmail.com
|
||||
EMAIL_TOKEN="sonv vwiu khse vtmv"
|
||||
|
||||
// nama email yang akan muncul ke user sebagai pengirim
|
||||
EMAIL_SENDER=no-reply@eigen.co.id
|
||||
|
||||
MIDTRANS_URL=https://app.sandbox.midtrans.com
|
||||
MIDTRANS_PRODUCTION=false
|
||||
MIDTRANS_SERVER_KEY=
|
||||
MIDTRANS_CLIENT_KEY=
|
||||
MIDTRANS_SERVER_KEY=SB-Mid-server-kH9_RBZrTwaUkxSrC5vOVaeG
|
||||
MIDTRANS_CLIENT_KEY=SB-Mid-client-7XLwqG5cgjUmZj-7
|
||||
|
||||
EXPORT_LIMIT_PARTITION=200
|
||||
ASSETS="https://asset.sky.eigen.co.id/"
|
||||
ASSETS="https://asset.sky.eigen.co.id/"
|
||||
|
||||
GOOGLE_CALENDAR_KEY="AIzaSyCSg4P3uC9Z7kD1P4f3rf1BbBaz4Q-M55o"
|
||||
GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec10bd5@group.calendar.google.com"
|
|
@ -22,15 +22,18 @@ CRON_MIDNIGHT="55 11 * * *"
|
|||
CRON_EVERY_MINUTE="55 11 * * *"
|
||||
CRON_EVERY_HOUR="0 * * * *"
|
||||
|
||||
EMAIL_HOST="sandbox.smtp.mailtrap.io"
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_POST=465
|
||||
EMAIL_USER=
|
||||
EMAIL_TOKEN=
|
||||
EMAIL_USER=weplayground.app@gmail.com
|
||||
EMAIL_TOKEN="sonv vwiu khse vtmv"
|
||||
|
||||
MIDTRANS_URL=https://app.midtrans.com
|
||||
MIDTRANS_PRODUCTION=true
|
||||
MIDTRANS_SERVER_KEY=
|
||||
MIDTRANS_CLIENT_KEY=
|
||||
MIDTRANS_SERVER_KEY=Mid-server-BZlPCcrWHDuSxW48oxBs5uAl
|
||||
MIDTRANS_CLIENT_KEY=Mid-client-YhOPuo0NZPNZfiKq
|
||||
|
||||
EXPORT_LIMIT_PARTITION=200
|
||||
ASSETS="https://asset.sky.eigen.co.id/"
|
||||
ASSETS="https://asset.sky.eigen.co.id/"
|
||||
|
||||
GOOGLE_CALENDAR_KEY="AIzaSyCSg4P3uC9Z7kD1P4f3rf1BbBaz4Q-M55o"
|
||||
GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec10bd5@group.calendar.google.com"
|
|
@ -53,6 +53,7 @@
|
|||
"moment": "^2.30.1",
|
||||
"nano": "^10.1.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"pdfmake": "^0.2.10",
|
||||
"pg": "^8.11.5",
|
||||
"plop": "^4.0.1",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
|
|
|
@ -69,6 +69,8 @@ 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';
|
||||
import { PosLogModel } from './modules/configuration/log/data/models/pos-log.model';
|
||||
import { ExportModule } from './modules/configuration/export/export.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -96,6 +98,7 @@ import { MailModule } from './modules/configuration/mail/mail.module';
|
|||
LogModel,
|
||||
NewsModel,
|
||||
PaymentMethodModel,
|
||||
PosLogModel,
|
||||
RefundModel,
|
||||
RefundItemModel,
|
||||
SalesPriceFormulaModel,
|
||||
|
@ -121,6 +124,7 @@ import { MailModule } from './modules/configuration/mail/mail.module';
|
|||
CqrsModule,
|
||||
CouchModule,
|
||||
CronModule,
|
||||
ExportModule,
|
||||
GoogleCalendarModule,
|
||||
LogModule,
|
||||
MailModule,
|
||||
|
|
|
@ -25,7 +25,7 @@ export class RolesGuard extends JWTGuard {
|
|||
if (isNotAllow) {
|
||||
throw new ForbiddenException({
|
||||
statusCode: 10003,
|
||||
message: `Forbidden Access, you don't have access to this module!`,
|
||||
message: `Akses Terlarang, anda tidak punya akses ke module ini!`,
|
||||
error: 'ACCESS_FORBIDDEN',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ export class PrivilegeService {
|
|||
if (!moduleKey) {
|
||||
throw new ForbiddenException({
|
||||
statusCode: 10005,
|
||||
message: `Forbidden Access, access Module is Require!`,
|
||||
message: `Akses Terlarang, anda tidak punya akses ke module ini!`,
|
||||
error: 'MODULE_KEY_NOT_FOUND',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -43,9 +43,9 @@ export class CheckDuplicateHelper {
|
|||
if (data_exists > 0) {
|
||||
throw new UnprocessableEntityException({
|
||||
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
message: `Entity with ${columnCheck.column} : ${
|
||||
message: `Gagal! Data dengan ${columnCheck.column} : ${
|
||||
this.entity[columnCheck.column]
|
||||
} already exist`,
|
||||
} telah ada`,
|
||||
error: 'Unprocessable Entity',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,12 +20,16 @@ export class SpecificSearchFilter<Entity = any> {
|
|||
new Brackets((qb) => {
|
||||
params.forEach((param) => {
|
||||
const { cols, data, additional, leftJoin } = param;
|
||||
const columns = cols.split('.');
|
||||
let arr = data;
|
||||
|
||||
const arr = data?.map((el) =>
|
||||
el.includes("'")
|
||||
? `'%${el.trim().replace(/'/g, "''").replace(/\s+/g, ' ')}%'`
|
||||
: `'%${el.trim().replace(/\s+/g, ' ')}%'`,
|
||||
);
|
||||
if (!columns.includes('status::text')) {
|
||||
arr = data?.map((el) =>
|
||||
el.includes("'")
|
||||
? `'%${el.trim().replace(/'/g, "''").replace(/\s+/g, ' ')}%'`
|
||||
: `'%${el.trim().replace(/\s+/g, ' ')}%'`,
|
||||
);
|
||||
}
|
||||
|
||||
const aliases = !cols.match(/\./g)
|
||||
? this.table.concat(`.${cols}`)
|
||||
|
|
|
@ -17,8 +17,8 @@ export abstract class BaseDataService<Entity> {
|
|||
entityTarget: EntityTarget<Entity>,
|
||||
entity: Entity,
|
||||
): Promise<Entity> {
|
||||
const newEntity = queryRunner.manager.create(entityTarget, entity);
|
||||
return await queryRunner.manager.save(newEntity);
|
||||
// const newEntity = this.repository.create(entityTarget, entity);
|
||||
return await this.repository.save(entity);
|
||||
}
|
||||
|
||||
async createMany(
|
||||
|
@ -26,8 +26,8 @@ export abstract class BaseDataService<Entity> {
|
|||
entityTarget: EntityTarget<Entity>,
|
||||
entity: Entity[],
|
||||
): Promise<Entity[]> {
|
||||
const newEntity = queryRunner.manager.create(entityTarget, entity);
|
||||
return await queryRunner.manager.save(newEntity);
|
||||
// const newEntity = this.repository.create(entityTarget, entity);
|
||||
return await this.repository.save(entity);
|
||||
}
|
||||
|
||||
async createBatch(
|
||||
|
@ -35,8 +35,8 @@ export abstract class BaseDataService<Entity> {
|
|||
entityTarget: EntityTarget<Entity>,
|
||||
entity: Entity[],
|
||||
): Promise<Entity[]> {
|
||||
const newEntity = queryRunner.manager.create(entityTarget, entity);
|
||||
return await queryRunner.manager.save(newEntity);
|
||||
// const newEntity = this.repository.create(entityTarget, entity);
|
||||
return await this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
|
@ -45,13 +45,13 @@ export abstract class BaseDataService<Entity> {
|
|||
filterUpdate: any,
|
||||
entity: Entity,
|
||||
): Promise<Entity> {
|
||||
const newEntity = await queryRunner.manager.findOne(entityTarget, {
|
||||
const newEntity = await this.repository.findOne({
|
||||
where: filterUpdate,
|
||||
});
|
||||
|
||||
if (!newEntity) throw new Error('Data not found!');
|
||||
Object.assign(newEntity, entity);
|
||||
return await queryRunner.manager.save(newEntity);
|
||||
return await this.repository.save(newEntity);
|
||||
}
|
||||
|
||||
async deleteById(
|
||||
|
@ -59,7 +59,15 @@ export abstract class BaseDataService<Entity> {
|
|||
entityTarget: EntityTarget<Entity>,
|
||||
id: string,
|
||||
): Promise<void> {
|
||||
await queryRunner.manager.delete(entityTarget, { id });
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
async deleteByIds(
|
||||
queryRunner: QueryRunner,
|
||||
entityTarget: EntityTarget<Entity>,
|
||||
ids: string[],
|
||||
): Promise<void> {
|
||||
await this.repository.delete(ids);
|
||||
}
|
||||
|
||||
async deleteByOptions(
|
||||
|
@ -67,11 +75,8 @@ export abstract class BaseDataService<Entity> {
|
|||
entityTarget: EntityTarget<Entity>,
|
||||
findManyOptions: FindManyOptions<Entity>,
|
||||
): Promise<void> {
|
||||
const datas = await queryRunner.manager.find(entityTarget, findManyOptions);
|
||||
await queryRunner.manager.delete(
|
||||
entityTarget,
|
||||
datas?.map((item) => item['id']),
|
||||
);
|
||||
const datas = await this.repository.find(findManyOptions);
|
||||
await this.repository.delete(datas?.map((item) => item['id']));
|
||||
}
|
||||
|
||||
async getOneByOptions(findOneOptions): Promise<Entity> {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export interface BaseCoreEntity {
|
||||
id: string;
|
||||
id?: string;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface Param {
|
|||
data: string[];
|
||||
additional?: any[];
|
||||
leftJoin?: any[];
|
||||
isStatus?: boolean;
|
||||
}
|
||||
|
||||
export interface RelationParam {
|
||||
|
|
|
@ -82,11 +82,11 @@ export abstract class BaseManager {
|
|||
|
||||
this.baseLog.verbose('commitTransaction');
|
||||
await this.queryRunner.commitTransaction();
|
||||
|
||||
await this.queryRunner.release();
|
||||
} catch (e) {
|
||||
if (e.response) throw new Error(JSON.stringify(e.response));
|
||||
else throw new Error(e.message);
|
||||
} finally {
|
||||
await this.queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ export abstract class BaseBatchDeleteManager<Entity> extends BaseManager {
|
|||
if (!entity) {
|
||||
throw new NotFoundException({
|
||||
statusCode: HttpStatus.NOT_FOUND,
|
||||
message: `Failed! Entity with id ${id} not found`,
|
||||
message: `Gagal! data dengan id ${id} tidak ditemukan`,
|
||||
error: 'Entity Not Found',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,6 +15,26 @@ export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
|
|||
abstract get entityTarget(): any;
|
||||
|
||||
setData(ids: string[], status: STATUS): void {
|
||||
/**
|
||||
* // TODO: Handle case confirm multiple tabs;
|
||||
* Pola ids yang dikirim dirubah menjadi data_id___updated_at
|
||||
* Untuk mendapatkan value id nya saja dan menghindari breaking change
|
||||
* karena sudah digunakan sebelumnya lakukan split dari data ids yang dikirim
|
||||
* Example:
|
||||
* this.dataIds = ids.map((i)=> {
|
||||
* return i.split('___')[0]
|
||||
* })
|
||||
*
|
||||
* Simpan data ids yang mempunyai update_at kedalam valiable baru
|
||||
* Example:
|
||||
* this.dataIdsWithDate = ids.map((i)=> {
|
||||
* return {
|
||||
* id: i.split('___')[0],
|
||||
* updated_at: i.split('___')[1]
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
|
||||
this.dataIds = ids;
|
||||
this.dataStatus = status;
|
||||
}
|
||||
|
@ -32,8 +52,21 @@ export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
|
|||
let totalSuccess = 0;
|
||||
const messages = [];
|
||||
|
||||
/**
|
||||
* // TODO: Handle case confirm multiple tabs;
|
||||
* Lopping data diambil dari dataIdsWithDate
|
||||
* exp: for (const item of this.dataIdsWithDate)
|
||||
*/
|
||||
|
||||
for (const id of this.dataIds) {
|
||||
try {
|
||||
/**
|
||||
* // TODO: Handle case confirm multiple tabs;
|
||||
* buat variable:
|
||||
* const id = item.id
|
||||
* const updated_at = item.updated_at
|
||||
*/
|
||||
|
||||
const entity = await this.dataService.getOneByOptions({
|
||||
where: {
|
||||
id: id,
|
||||
|
@ -44,19 +77,19 @@ export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
|
|||
if (!entity) {
|
||||
throw new NotFoundException({
|
||||
statusCode: HttpStatus.NOT_FOUND,
|
||||
message: `Failed! Entity with id ${id} not found`,
|
||||
message: `Gagal! data dengan id ${id} tidak ditemukan`,
|
||||
error: 'Entity Not Found',
|
||||
});
|
||||
}
|
||||
this.oldData = _.cloneDeep(entity);
|
||||
await this.validateData(entity);
|
||||
|
||||
Object.assign(entity, {
|
||||
status: this.dataStatus,
|
||||
editor_id: this.user.id,
|
||||
editor_name: this.user.name,
|
||||
updated_at: new Date().getTime(),
|
||||
});
|
||||
|
||||
await this.validateData(entity);
|
||||
await new ValidateRelationHelper(
|
||||
id,
|
||||
this.dataService,
|
||||
|
@ -64,6 +97,14 @@ export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
|
|||
this.tableName,
|
||||
).execute();
|
||||
|
||||
/**
|
||||
* // TODO: Handle case confirm multiple tabs;
|
||||
* lakukan update data dengan where condition id dan updated_at
|
||||
* EXPECTATION => status akan berubah jika updated_at yang dikirim dari FE sama dengen yang di database
|
||||
* IF => updated_at beda tidak perlu melakukan update status dan tidak perlu memanggil eventBus tetapi tetap dihitung sebagai aksi yang berhasil
|
||||
* IF => FE tidak menambahkan updated_at makan lakukan update dan publishEvent
|
||||
*/
|
||||
|
||||
const result = await this.dataService.update(
|
||||
this.queryRunner,
|
||||
this.entityTarget,
|
||||
|
|
|
@ -29,7 +29,7 @@ export abstract class BaseChangePosition<Entity> extends BaseManager {
|
|||
if (!this.data?.end || this.data.start == this.data?.end) {
|
||||
throw new UnprocessableEntityException({
|
||||
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
message: 'Please drag to another position',
|
||||
message: 'Gagal! tolong pindahkan ke posisi lain',
|
||||
error: 'Unprocessable Entity',
|
||||
});
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ export abstract class BaseChangePosition<Entity> extends BaseManager {
|
|||
if (!this.startData) {
|
||||
throw new UnprocessableEntityException({
|
||||
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
message: `Entity with id : ${this.data.start} not found`,
|
||||
message: `Gagal! data dengan id : ${this.data.start} tidak ditemukan`,
|
||||
error: 'Unprocessable Entity',
|
||||
});
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export abstract class BaseChangePosition<Entity> extends BaseManager {
|
|||
if (!this.endData) {
|
||||
throw new UnprocessableEntityException({
|
||||
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
message: `Entity with id : ${this.data.end} not found`,
|
||||
message: `Gagal! data dengan id : ${this.data.end} tidak ditemukan`,
|
||||
error: 'Unprocessable Entity',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ export abstract class BaseCreateManager<Entity> extends BaseManager {
|
|||
|
||||
this.eventBus.publishAll([
|
||||
new topic.topic({
|
||||
id: data?.['id'] ?? topic?.data?.['id'],
|
||||
id: this.result['id'],
|
||||
old: null,
|
||||
data: data ?? topic.data,
|
||||
user: this.user,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { validateRelations } from 'src/core/strings/constants/interface.constants';
|
||||
import { BaseManager } from '../base.manager';
|
||||
import { OPERATION } from 'src/core/strings/constants/base.constants';
|
||||
|
||||
export abstract class BaseCustomManager<Entity> extends BaseManager {
|
||||
protected result: any;
|
||||
|
@ -23,4 +24,31 @@ export abstract class BaseCustomManager<Entity> extends BaseManager {
|
|||
}
|
||||
|
||||
abstract getResult(): any;
|
||||
|
||||
async publishEvents() {
|
||||
if (!this.eventTopics.length) return;
|
||||
for (const topic of this.eventTopics) {
|
||||
let data;
|
||||
if (!topic.data) {
|
||||
data = await this.dataService.getOneByOptions({
|
||||
where: {
|
||||
id: this.result['id'],
|
||||
},
|
||||
relations: topic.relations,
|
||||
});
|
||||
}
|
||||
|
||||
this.eventBus.publishAll([
|
||||
new topic.topic({
|
||||
id: data?.['id'] ?? topic?.data?.['id'],
|
||||
old: null,
|
||||
data: data ?? topic.data,
|
||||
user: this.user,
|
||||
description: '',
|
||||
module: this.tableName,
|
||||
op: OPERATION.UPDATE,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export abstract class BaseDeleteManager<Entity> extends BaseManager {
|
|||
if (!this.data)
|
||||
throw new UnprocessableEntityException({
|
||||
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
message: `Data with id ${this.dataId} not found`,
|
||||
message: `Gagal! Data denga id ${this.dataId} tidak ditemukan`,
|
||||
error: 'Unprocessable Entity',
|
||||
});
|
||||
|
||||
|
|
|
@ -50,12 +50,16 @@ export abstract class BaseIndexManager<Entity> extends BaseReadManager {
|
|||
|
||||
// jika searching status terdapat dalam enum, maka dia mencari specific data
|
||||
// ? karena jika tidak, ketika dia search "active" maka "inactive" juga ikut
|
||||
return STATUS[statusData.toUpperCase()] ?? statusData;
|
||||
});
|
||||
specificFilter.push({
|
||||
cols: `${this.tableName}.status::text`,
|
||||
data: data,
|
||||
return `'${STATUS[statusData.toUpperCase()]}'` ?? `'%${statusData}%'`;
|
||||
});
|
||||
|
||||
const exist = specificFilter.find((item) => item.isStatus);
|
||||
if (!exist) {
|
||||
specificFilter.push({
|
||||
cols: `${this.tableName}.status::text`,
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new SpecificSearchFilter<Entity>(
|
||||
|
|
|
@ -14,6 +14,22 @@ export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
|
|||
abstract get entityTarget(): any;
|
||||
|
||||
setData(id: string, status: STATUS): void {
|
||||
/**
|
||||
* // TODO: Handle case confirm multiple tabs;
|
||||
* Pola id yang dikirim dirubah menjadi data_id___updated_at
|
||||
* Untuk mendapatkan value id nya saja dan menghindari breaking change
|
||||
* karena sudah digunakan sebelumnya lakukan split dari data ids yang dikirim
|
||||
* Example:
|
||||
* this.dataId = id.split('___')[0]
|
||||
*
|
||||
* Simpan data id yang mempunyai update_at kedalam valiable baru
|
||||
* Example:
|
||||
* this.dataIdsWithDate = {
|
||||
* id: id.split('___')[0],
|
||||
* updated_at: id.split('___')[1]
|
||||
* }
|
||||
*/
|
||||
|
||||
this.dataId = id;
|
||||
this.dataStatus = status;
|
||||
}
|
||||
|
@ -43,6 +59,16 @@ export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
|
|||
this.tableName,
|
||||
).execute();
|
||||
|
||||
/**
|
||||
* // TODO: Handle case confirm multiple tabs;
|
||||
* IF => updated_at sama dengen data yang di database
|
||||
* THEN =>
|
||||
* - Lakukan update data dengan where condition id dan updated_at
|
||||
* - EXPECTATION = > status akan berubah jika updated_at yang dikirim dari FE sama dengen yang di database
|
||||
* IF => updated_at beda maka retun curent data tanpa harus malakukan update status dan publish event
|
||||
* IF => FE tidak menambahkan updated_at makan lakukan update dan publishEvent
|
||||
*/
|
||||
|
||||
this.result = await this.dataService.update(
|
||||
this.queryRunner,
|
||||
this.entityTarget,
|
||||
|
|
|
@ -27,7 +27,7 @@ export abstract class BaseUpdateManager<Entity> extends BaseManager {
|
|||
if (!this.oldData) {
|
||||
throw new NotFoundException({
|
||||
statusCode: HttpStatus.NOT_FOUND,
|
||||
message: `Failed! Entity with id ${this.dataId} not found`,
|
||||
message: `Gagal! Data denga id ${this.dataId} tidak ditemukan`,
|
||||
error: 'Entity Not Found',
|
||||
});
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ export abstract class BaseUpdateManager<Entity> extends BaseManager {
|
|||
|
||||
this.eventBus.publishAll([
|
||||
new topic.topic({
|
||||
id: data?.['id'] ?? topic?.data?.['id'],
|
||||
id: topic.data?.['id'] ?? this.dataId,
|
||||
old: this.oldData,
|
||||
data: data ?? topic.data,
|
||||
user: this.user,
|
||||
|
|
|
@ -20,6 +20,16 @@ export enum ORDER_TYPE {
|
|||
DESC = 'DESC',
|
||||
}
|
||||
|
||||
export const DAY = [
|
||||
'minggu',
|
||||
'senin',
|
||||
'selasa',
|
||||
'rabu',
|
||||
'kamis',
|
||||
'jumat',
|
||||
'sabtu',
|
||||
];
|
||||
|
||||
export enum CONNECTION_NAME {
|
||||
DEFAULT = 'default',
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@ export const PrivilegeAdminConstant = [
|
|||
menu_label: 'Rekonsiliasi',
|
||||
actions: [
|
||||
PrivilegeAction.VIEW,
|
||||
PrivilegeAction.CREATE,
|
||||
PrivilegeAction.CONFIRM,
|
||||
PrivilegeAction.DELETE,
|
||||
PrivilegeAction.CANCEL,
|
||||
|
@ -137,6 +136,12 @@ export const PrivilegeAdminConstant = [
|
|||
actions: [PrivilegeAction.CREATE],
|
||||
index: 13,
|
||||
},
|
||||
{
|
||||
menu: 'DOWNLOAD_POS_APP',
|
||||
menu_label: 'Download POS App',
|
||||
actions: [PrivilegeAction.VIEW],
|
||||
index: 20,
|
||||
},
|
||||
];
|
||||
|
||||
export const PrivilegePOSConstant = [
|
||||
|
@ -146,8 +151,8 @@ export const PrivilegePOSConstant = [
|
|||
actions: [
|
||||
PrivilegeAction.VIEW,
|
||||
PrivilegeAction.CREATE,
|
||||
PrivilegeAction.DELETE,
|
||||
PrivilegeAction.EDIT,
|
||||
PrivilegeAction.CANCEL,
|
||||
],
|
||||
index: 14,
|
||||
},
|
||||
|
@ -160,7 +165,7 @@ export const PrivilegePOSConstant = [
|
|||
{
|
||||
menu: 'BOOKING',
|
||||
menu_label: 'Pemesanan',
|
||||
actions: [PrivilegeAction.VIEW, PrivilegeAction.CREATE],
|
||||
actions: [PrivilegeAction.VIEW],
|
||||
index: 16,
|
||||
},
|
||||
{
|
||||
|
@ -175,4 +180,10 @@ export const PrivilegePOSConstant = [
|
|||
actions: [PrivilegeAction.CREATE],
|
||||
index: 18,
|
||||
},
|
||||
{
|
||||
menu: 'PRINT_RECEIPT',
|
||||
menu_label: 'Print Receipt',
|
||||
actions: [PrivilegeAction.CREATE],
|
||||
index: 19,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -7,6 +7,7 @@ export enum TABLE_NAME {
|
|||
ITEM_RATE = 'item_rates',
|
||||
GATE = 'gates',
|
||||
LOG = 'logs',
|
||||
LOG_POS = 'logs_pos',
|
||||
NEWS = 'news',
|
||||
PAYMENT_METHOD = 'payment_methods',
|
||||
PRICE_FORMULA = 'price_formulas',
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class PosLog1721736523991 implements MigrationInterface {
|
||||
name = 'PosLog1721736523991';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "logs_pos" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying NOT NULL DEFAULT 'cash withdrawal', "pos_number" bigint, "total_balance" numeric, "created_at" bigint, "creator_name" character varying, "creator_id" character varying, CONSTRAINT "PK_60df825558a6b6881d7ad770d26" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "logs_pos"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddCalendarColumnTransaction1721892389807
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddCalendarColumnTransaction1721892389807';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" ADD "calendar_id" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" ADD "calendar_link" character varying`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" DROP COLUMN "calendar_id"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" DROP COLUMN "calendar_link"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddColumnToRefundTable1722318939681 implements MigrationInterface {
|
||||
name = 'AddColumnToRefundTable1722318939681';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "public"."refunds_refund_reason_type_enum" AS ENUM('weather', 'ride malfunction', 'other')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refunds" ADD "refund_reason_type" "public"."refunds_refund_reason_type_enum" NOT NULL DEFAULT 'ride malfunction'`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "refunds" ADD "refund_reason" text`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refunds" DROP COLUMN "refund_reason"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refunds" DROP COLUMN "refund_reason_type"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "public"."refunds_refund_reason_type_enum"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateColumnToTransactionTable1722334034920
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'UpdateColumnToTransactionTable1722334034920';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transaction_items" ADD "qr_image_url" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" ADD "payment_code" character varying`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" DROP COLUMN "payment_code"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transaction_items" DROP COLUMN "qr_image_url"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class PosLogAddColumnDownBy1722509262047 implements MigrationInterface {
|
||||
name = 'PosLogAddColumnDownBy1722509262047';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "logs_pos" ADD "drawn_by_name" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "logs_pos" ADD "drawn_by_id" character varying`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "logs_pos" DROP COLUMN "drawn_by_id"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "logs_pos" DROP COLUMN "drawn_by_name"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateRelationTableTransaction1722581313837
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'UpdateRelationTableTransaction1722581313837';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refunds" DROP CONSTRAINT "FK_8bb3b7579f49990d2e77684acd4"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refunds" DROP CONSTRAINT "REL_8bb3b7579f49990d2e77684acd"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refund_items" DROP CONSTRAINT "FK_07b481a163c219f5de8fb1c90b3"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refund_items" DROP CONSTRAINT "REL_07b481a163c219f5de8fb1c90b"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refunds" ADD CONSTRAINT "FK_8bb3b7579f49990d2e77684acd4" FOREIGN KEY ("transaction_id") REFERENCES "transactions"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refund_items" ADD CONSTRAINT "FK_07b481a163c219f5de8fb1c90b3" FOREIGN KEY ("transaction_item_id") REFERENCES "transaction_items"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refund_items" DROP CONSTRAINT "FK_07b481a163c219f5de8fb1c90b3"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refunds" DROP CONSTRAINT "FK_8bb3b7579f49990d2e77684acd4"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refund_items" ADD CONSTRAINT "REL_07b481a163c219f5de8fb1c90b" UNIQUE ("transaction_item_id")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refund_items" ADD CONSTRAINT "FK_07b481a163c219f5de8fb1c90b3" FOREIGN KEY ("transaction_item_id") REFERENCES "transaction_items"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refunds" ADD CONSTRAINT "REL_8bb3b7579f49990d2e77684acd" UNIQUE ("transaction_id")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refunds" ADD CONSTRAINT "FK_8bb3b7579f49990d2e77684acd4" FOREIGN KEY ("transaction_id") REFERENCES "transactions"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateTypeColumnItemTable1722587128195
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'UpdateTypeColumnItemTable1722587128195';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "items" DROP COLUMN "sales_margin"`);
|
||||
await queryRunner.query(`ALTER TABLE "items" ADD "sales_margin" numeric`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "items" DROP COLUMN "sales_margin"`);
|
||||
await queryRunner.query(`ALTER TABLE "items" ADD "sales_margin" integer`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateTableTransaction1722595038215 implements MigrationInterface {
|
||||
name = 'UpdateTableTransaction1722595038215';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "public"."transactions_payment_type_counter_enum" AS ENUM('midtrans', 'bank transfer', 'qris', 'counter', 'cash', 'credit card', 'debit', 'e-money')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" ADD "payment_type_counter" "public"."transactions_payment_type_counter_enum"`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" DROP COLUMN "payment_type_counter"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "public"."transactions_payment_type_counter_enum"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddColumnToTransactionsTable1722693550579
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddColumnToTransactionsTable1722693550579';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" ADD "booking_date_before" date`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "transactions" DROP COLUMN "booking_date_before"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UniqueNameItem1722922766205 implements MigrationInterface {
|
||||
name = 'UniqueNameItem1722922766205';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "item_bundlings" DROP CONSTRAINT "FK_a50e7abf2caba4d0394f3726b86"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "items" ADD CONSTRAINT "UQ_213736582899b3599acaade2cd1" UNIQUE ("name")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "item_bundlings" ADD CONSTRAINT "FK_a50e7abf2caba4d0394f3726b86" FOREIGN KEY ("item_bundling_id") REFERENCES "items"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "item_bundlings" DROP CONSTRAINT "FK_a50e7abf2caba4d0394f3726b86"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "items" DROP CONSTRAINT "UQ_213736582899b3599acaade2cd1"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "item_bundlings" ADD CONSTRAINT "FK_a50e7abf2caba4d0394f3726b86" FOREIGN KEY ("item_bundling_id") REFERENCES "items"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -133,7 +133,7 @@ export class LoginManager extends BaseCustomManager<UserEntity> {
|
|||
throwError() {
|
||||
throw new UnauthorizedException({
|
||||
statusCode: HttpStatus.UNAUTHORIZED,
|
||||
message: `Failed! You have entered an invalid username or password`,
|
||||
message: `Gagal! username atau password tidak sesuai`,
|
||||
error: 'Unauthorized',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,8 +5,12 @@ import { STATUS } from 'src/core/strings/constants/base.constants';
|
|||
import { ItemType } from 'src/modules/item-related/item-category/constants';
|
||||
import { LimitType } from 'src/modules/item-related/item/constants';
|
||||
import { PaymentMethodType } from 'src/modules/transaction/payment-method/constants';
|
||||
import { RefundType } from 'src/modules/transaction/refund/constants';
|
||||
import {
|
||||
RefundReasonType,
|
||||
RefundType,
|
||||
} from 'src/modules/transaction/refund/constants';
|
||||
import { GateType } from 'src/modules/web-information/gate/constants';
|
||||
import { InvoiceType } from '../../export/constants';
|
||||
|
||||
@ApiTags('configuration - constant')
|
||||
@Controller('v1/constant')
|
||||
|
@ -52,8 +56,18 @@ export class ConstantController {
|
|||
return Object.values(RefundType);
|
||||
}
|
||||
|
||||
@Get('refund-reason-type')
|
||||
async refundReasonType(): Promise<any> {
|
||||
return Object.values(RefundReasonType);
|
||||
}
|
||||
|
||||
@Get('gate-type')
|
||||
async gateType(): Promise<any> {
|
||||
return Object.values(GateType);
|
||||
}
|
||||
|
||||
@Get('invoice-type')
|
||||
async invoiceType(): Promise<any> {
|
||||
return Object.values(InvoiceType);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,6 @@
|
|||
export const DatabaseListen = ['transaction', 'vip_code'];
|
||||
export const DatabaseListen = [
|
||||
'transaction',
|
||||
'vip_code',
|
||||
'pos_activity',
|
||||
'pos_cash_activity',
|
||||
];
|
||||
|
|
|
@ -17,10 +17,13 @@ import {
|
|||
} from './domain/managers/season-period.handler';
|
||||
import {
|
||||
ItemDeletedHandler,
|
||||
ItemPriceUpdatedHandler,
|
||||
ItemRateUpdatedHandler,
|
||||
ItemUpdatedHandler,
|
||||
} from './domain/managers/item.handler';
|
||||
import {
|
||||
UserDeletedHandler,
|
||||
UserPrivilegeUpdateHandler,
|
||||
UserUpdatedHandler,
|
||||
} from './domain/managers/user.handler';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
@ -31,13 +34,22 @@ import { UserDataService } from 'src/modules/user-related/user/data/services/use
|
|||
import { ItemDataService } from 'src/modules/item-related/item/data/services/item-data.service';
|
||||
import {
|
||||
BookingDeletedEvent,
|
||||
BookingHandler,
|
||||
// BookingHandler,
|
||||
BookingUpdateHandler,
|
||||
ChangeStatusBookingHandler,
|
||||
} from './domain/managers/booking.handler';
|
||||
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
|
||||
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
|
||||
import { TransactionTaxModel } from 'src/modules/transaction/transaction/data/models/transaction-tax.model';
|
||||
import { TransactionItemModel } from 'src/modules/transaction/transaction/data/models/transaction-item.model';
|
||||
import { VipCodeCreatedHandler } from './domain/managers/vip-code.handler';
|
||||
import { ItemRateModel } from 'src/modules/item-related/item-rate/data/models/item-rate.model';
|
||||
import {
|
||||
SeasonTypeDeletedHandler,
|
||||
SeasonTypeUpdatedHandler,
|
||||
} from './domain/managers/season-type.handler';
|
||||
import { SeasonPeriodDataService } from 'src/modules/season-related/season-period/data/services/season-period-data.service';
|
||||
import { SeasonPeriodModel } from 'src/modules/season-related/season-period/data/models/season-period.model';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -45,6 +57,8 @@ import { VipCodeCreatedHandler } from './domain/managers/vip-code.handler';
|
|||
TypeOrmModule.forFeature(
|
||||
[
|
||||
ItemModel,
|
||||
ItemRateModel,
|
||||
SeasonPeriodModel,
|
||||
UserModel,
|
||||
TransactionModel,
|
||||
TransactionTaxModel,
|
||||
|
@ -56,7 +70,9 @@ import { VipCodeCreatedHandler } from './domain/managers/vip-code.handler';
|
|||
],
|
||||
controllers: [CouchDataController],
|
||||
providers: [
|
||||
BookingHandler,
|
||||
// BookingHandler,
|
||||
BookingUpdateHandler,
|
||||
ChangeStatusBookingHandler,
|
||||
BookingDeletedEvent,
|
||||
PaymentMethodDeletedHandler,
|
||||
PaymentMethodUpdatedHandler,
|
||||
|
@ -67,9 +83,16 @@ import { VipCodeCreatedHandler } from './domain/managers/vip-code.handler';
|
|||
SeasonPeriodUpdatedHandler,
|
||||
ItemUpdatedHandler,
|
||||
ItemDeletedHandler,
|
||||
ItemRateUpdatedHandler,
|
||||
ItemPriceUpdatedHandler,
|
||||
UserDeletedHandler,
|
||||
UserUpdatedHandler,
|
||||
UserPrivilegeUpdateHandler,
|
||||
|
||||
SeasonTypeDeletedHandler,
|
||||
SeasonTypeUpdatedHandler,
|
||||
|
||||
SeasonPeriodDataService,
|
||||
TransactionDataService,
|
||||
UserDataService,
|
||||
ItemDataService,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { DatabaseListen } from '../../constants';
|
|||
import { EventBus } from '@nestjs/cqrs';
|
||||
import { ChangeDocEvent } from '../../domain/events/change-doc.event';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { apm } from 'src/core/apm';
|
||||
import * as Nano from 'nano';
|
||||
|
||||
@Injectable()
|
||||
|
@ -23,6 +23,10 @@ export class CouchService {
|
|||
for (const database of DatabaseListen) {
|
||||
const db = nano.db.use(database);
|
||||
db.changesReader.start({ includeDocs: true }).on('change', (change) => {
|
||||
Logger.log(
|
||||
`Receive Data from ${database}: ${change?.id}`,
|
||||
'CouchService',
|
||||
);
|
||||
this.changeDoc(change, database);
|
||||
});
|
||||
|
||||
|
@ -45,7 +49,10 @@ export class CouchService {
|
|||
const nano = this.nanoInstance;
|
||||
const db = nano.use(database);
|
||||
return await db.insert(data);
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
apm.captureError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteDoc(data, database) {
|
||||
|
@ -54,7 +61,10 @@ export class CouchService {
|
|||
const db = nano.use(database);
|
||||
const result = await db.get(data.id);
|
||||
await db.destroy(data.id, result._rev);
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
apm.captureError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateDoc(data, database) {
|
||||
|
@ -62,11 +72,26 @@ export class CouchService {
|
|||
const nano = this.nanoInstance;
|
||||
const db = nano.use(database);
|
||||
const result = await db.get(data.id);
|
||||
console.log(result, 'dsa');
|
||||
await db.insert({
|
||||
...data,
|
||||
_rev: result._rev,
|
||||
});
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
apm.captureError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async getDoc(id: string, database: string) {
|
||||
try {
|
||||
const nano = this.nanoInstance;
|
||||
const db = nano.use(database);
|
||||
const result = await db.get(id);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
apm.captureError(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,7 @@ import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
|
|||
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 { CouchService } from '../../data/services/couch.service';
|
||||
import { STATUS } from 'src/core/strings/constants/base.constants';
|
||||
import { TransactionPaymentType } from 'src/modules/transaction/transaction/constants';
|
||||
// import { STATUS } from 'src/core/strings/constants/base.constants';
|
||||
import { mappingTransaction } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/mapping-transaction.helper';
|
||||
import { TransactionDeletedEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-deleted.event';
|
||||
import { TransactionUpdatedEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-updated.event';
|
||||
|
@ -20,13 +19,65 @@ export class BookingDeletedEvent
|
|||
_id: event.data.id,
|
||||
...event.data.data,
|
||||
},
|
||||
'item',
|
||||
'booking',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@EventsHandler(TransactionChangeStatusEvent, TransactionUpdatedEvent)
|
||||
export class BookingHandler
|
||||
// @EventsHandler(TransactionChangeStatusEvent, TransactionUpdatedEvent)
|
||||
// export class BookingHandler
|
||||
// implements IEventHandler<TransactionChangeStatusEvent>
|
||||
// {
|
||||
// constructor(
|
||||
// private bookingService: TransactionDataService,
|
||||
// private couchService: CouchService,
|
||||
// ) {}
|
||||
|
||||
// async handle(event: TransactionChangeStatusEvent) {
|
||||
// const old_data = event.data.old;
|
||||
// const data = event.data.data;
|
||||
|
||||
// if (
|
||||
// data.payment_type == TransactionPaymentType.COUNTER ||
|
||||
// ([STATUS.ACTIVE, STATUS.SETTLED].includes(data.status) &&
|
||||
// data.payment_type != TransactionPaymentType.COUNTER)
|
||||
// ) {
|
||||
// const booking = await this.bookingService.getOneByOptions({
|
||||
// where: {
|
||||
// id: data.id,
|
||||
// },
|
||||
// relations: ['items'],
|
||||
// });
|
||||
|
||||
// mappingTransaction(booking);
|
||||
|
||||
// if (
|
||||
// (old_data?.status != data.status ||
|
||||
// data.payment_type != TransactionPaymentType.COUNTER) &&
|
||||
// [STATUS.PENDING, STATUS.ACTIVE, STATUS.SETTLED].includes(data.status)
|
||||
// ) {
|
||||
// await this.couchService.createDoc(
|
||||
// {
|
||||
// _id: booking.id,
|
||||
// ...booking,
|
||||
// },
|
||||
// 'booking',
|
||||
// );
|
||||
// } else {
|
||||
// await this.couchService.updateDoc(
|
||||
// {
|
||||
// _id: booking.id,
|
||||
// ...booking,
|
||||
// },
|
||||
// 'booking',
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@EventsHandler(TransactionChangeStatusEvent)
|
||||
export class ChangeStatusBookingHandler
|
||||
implements IEventHandler<TransactionChangeStatusEvent>
|
||||
{
|
||||
constructor(
|
||||
|
@ -35,24 +86,23 @@ export class BookingHandler
|
|||
) {}
|
||||
|
||||
async handle(event: TransactionChangeStatusEvent) {
|
||||
const old_data = event.data.old;
|
||||
const data = event.data.data;
|
||||
const dataID = data?.id ?? data?.order_id;
|
||||
|
||||
if (data.payment_type != TransactionPaymentType.COUNTER) return;
|
||||
const couchData = await this.couchService.getDoc(dataID, 'booking');
|
||||
|
||||
const booking = await this.bookingService.getOneByOptions({
|
||||
where: {
|
||||
id: data.id,
|
||||
id: dataID,
|
||||
},
|
||||
relations: ['items'],
|
||||
});
|
||||
|
||||
console.log('change status', { dataID, couchData, booking });
|
||||
mappingTransaction(booking);
|
||||
console.log('after mapping');
|
||||
|
||||
if (
|
||||
old_data?.status != data.status &&
|
||||
[STATUS.PENDING, STATUS.ACTIVE].includes(data.status)
|
||||
) {
|
||||
if (!couchData) {
|
||||
console.log('save data to couch');
|
||||
await this.couchService.createDoc(
|
||||
{
|
||||
_id: booking.id,
|
||||
|
@ -61,6 +111,42 @@ export class BookingHandler
|
|||
'booking',
|
||||
);
|
||||
} else {
|
||||
console.log('update data to couch');
|
||||
await this.couchService.updateDoc(
|
||||
{
|
||||
_id: booking.id,
|
||||
...booking,
|
||||
},
|
||||
'booking',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventsHandler(TransactionUpdatedEvent)
|
||||
export class BookingUpdateHandler
|
||||
implements IEventHandler<TransactionUpdatedEvent>
|
||||
{
|
||||
constructor(
|
||||
private bookingService: TransactionDataService,
|
||||
private couchService: CouchService,
|
||||
) {}
|
||||
|
||||
async handle(event: TransactionUpdatedEvent) {
|
||||
const data = event.data.data;
|
||||
const dataID = data?.id ?? data?.order_id;
|
||||
|
||||
const couchData = await this.couchService.getDoc(dataID, 'booking');
|
||||
console.log('update', { dataID, couchData });
|
||||
if (couchData) {
|
||||
const booking = await this.bookingService.getOneByOptions({
|
||||
where: {
|
||||
id: dataID,
|
||||
},
|
||||
relations: ['items'],
|
||||
});
|
||||
console.log({ booking });
|
||||
mappingTransaction(booking);
|
||||
await this.couchService.updateDoc(
|
||||
{
|
||||
_id: booking.id,
|
||||
|
|
|
@ -5,6 +5,9 @@ import { ItemDeletedEvent } from 'src/modules/item-related/item/domain/entities/
|
|||
import { ItemUpdatedEvent } from 'src/modules/item-related/item/domain/entities/event/item-updated.event';
|
||||
import { ItemChangeStatusEvent } from 'src/modules/item-related/item/domain/entities/event/item-change-status.event';
|
||||
import { ItemDataService } from 'src/modules/item-related/item/data/services/item-data.service';
|
||||
import { SeasonPeriodUpdatedEvent } from 'src/modules/season-related/season-period/domain/entities/event/season-period-updated.event';
|
||||
import { SeasonPeriodChangeStatusEvent } from 'src/modules/season-related/season-period/domain/entities/event/season-period-change-status.event';
|
||||
import { ItemRateUpdatedEvent } from 'src/modules/item-related/item-rate/domain/entities/event/item-rate-updated.event';
|
||||
|
||||
@EventsHandler(ItemDeletedEvent)
|
||||
export class ItemDeletedHandler implements IEventHandler<ItemDeletedEvent> {
|
||||
|
@ -79,3 +82,85 @@ export class ItemUpdatedHandler
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventsHandler(SeasonPeriodChangeStatusEvent, SeasonPeriodUpdatedEvent)
|
||||
export class ItemPriceUpdatedHandler
|
||||
implements IEventHandler<SeasonPeriodChangeStatusEvent>
|
||||
{
|
||||
constructor(
|
||||
private couchService: CouchService,
|
||||
private itemService: ItemDataService,
|
||||
) {}
|
||||
|
||||
async handle(event: SeasonPeriodChangeStatusEvent) {
|
||||
const data = event.data.data;
|
||||
|
||||
// change status to active
|
||||
if (data.status == STATUS.ACTIVE) {
|
||||
const dataItems = await this.itemService.getManyByOptions({
|
||||
where: {
|
||||
status: STATUS.ACTIVE,
|
||||
},
|
||||
relations: [
|
||||
'item_category',
|
||||
'bundling_items',
|
||||
'bundling_items.item_category',
|
||||
'item_rates',
|
||||
'item_rates.item',
|
||||
'item_rates.season_period',
|
||||
'item_rates.season_period.season_type',
|
||||
],
|
||||
});
|
||||
|
||||
for (const dataItem of dataItems) {
|
||||
await this.couchService.updateDoc(
|
||||
{
|
||||
_id: dataItem.id,
|
||||
...dataItem,
|
||||
},
|
||||
'item',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventsHandler(ItemRateUpdatedEvent)
|
||||
export class ItemRateUpdatedHandler
|
||||
implements IEventHandler<ItemRateUpdatedEvent>
|
||||
{
|
||||
constructor(
|
||||
private couchService: CouchService,
|
||||
private itemService: ItemDataService,
|
||||
) {}
|
||||
|
||||
async handle(event: ItemRateUpdatedEvent) {
|
||||
const data = event.data.data;
|
||||
|
||||
const dataItems = await this.itemService.getManyByOptions({
|
||||
where: {
|
||||
status: STATUS.ACTIVE,
|
||||
id: data.item?.id,
|
||||
},
|
||||
relations: [
|
||||
'item_category',
|
||||
'bundling_items',
|
||||
'bundling_items.item_category',
|
||||
'item_rates',
|
||||
'item_rates.item',
|
||||
'item_rates.season_period',
|
||||
'item_rates.season_period.season_type',
|
||||
],
|
||||
});
|
||||
|
||||
for (const dataItem of dataItems) {
|
||||
await this.couchService.updateDoc(
|
||||
{
|
||||
_id: dataItem.id,
|
||||
...dataItem,
|
||||
},
|
||||
'item',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
|
||||
import { CouchService } from '../../data/services/couch.service';
|
||||
import { SeasonTypeDeletedEvent } from 'src/modules/season-related/season-type/domain/entities/event/season-type-deleted.event';
|
||||
import { SeasonTypeChangeStatusEvent } from 'src/modules/season-related/season-type/domain/entities/event/season-type-change-status.event';
|
||||
import { SeasonTypeUpdatedEvent } from 'src/modules/season-related/season-type/domain/entities/event/season-type-updated.event';
|
||||
import { SeasonPeriodDataService } from 'src/modules/season-related/season-period/data/services/season-period-data.service';
|
||||
|
||||
@EventsHandler(SeasonTypeDeletedEvent)
|
||||
export class SeasonTypeDeletedHandler
|
||||
implements IEventHandler<SeasonTypeDeletedEvent>
|
||||
{
|
||||
constructor(private couchService: CouchService) {}
|
||||
|
||||
async handle(event: SeasonTypeDeletedEvent) {
|
||||
console.log('deleted session type');
|
||||
}
|
||||
}
|
||||
|
||||
@EventsHandler(SeasonTypeChangeStatusEvent, SeasonTypeUpdatedEvent)
|
||||
export class SeasonTypeUpdatedHandler
|
||||
implements IEventHandler<SeasonTypeChangeStatusEvent>
|
||||
{
|
||||
constructor(
|
||||
private couchService: CouchService,
|
||||
private seasonPeriodService: SeasonPeriodDataService,
|
||||
) {}
|
||||
|
||||
async handle(event: SeasonTypeChangeStatusEvent) {
|
||||
const data = event.data.data;
|
||||
const typeID = data.id;
|
||||
const periods = await this.seasonPeriodService.getManyByOptions({
|
||||
where: {
|
||||
season_type_id: typeID,
|
||||
},
|
||||
relations: ['season_type'],
|
||||
});
|
||||
|
||||
for (const period of periods) {
|
||||
const dataID = period.id;
|
||||
const couchData = await this.couchService.getDoc(dataID, 'season_period');
|
||||
if (couchData) {
|
||||
await this.couchService.updateDoc(
|
||||
{
|
||||
_id: dataID,
|
||||
...period,
|
||||
},
|
||||
'season_period',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import { UserDeletedEvent } from 'src/modules/user-related/user/domain/entities/
|
|||
import { UserChangeStatusEvent } from 'src/modules/user-related/user/domain/entities/event/user-change-status.event';
|
||||
import { UserUpdatedEvent } from 'src/modules/user-related/user/domain/entities/event/user-updated.event';
|
||||
import { UserDataService } from 'src/modules/user-related/user/data/services/user-data.service';
|
||||
import { UserPrivilegeConfigUpdatedEvent } from 'src/modules/user-related/user-privilege/domain/entities/event/user-privilege-configuration-updated.event';
|
||||
|
||||
@EventsHandler(UserDeletedEvent)
|
||||
export class UserDeletedHandler implements IEventHandler<UserDeletedEvent> {
|
||||
|
@ -44,14 +45,16 @@ export class UserUpdatedHandler
|
|||
],
|
||||
})
|
||||
.then((item) => {
|
||||
const user_privilege_configurations = item[
|
||||
'user_privilege'
|
||||
]?.user_privilege_configurations?.filter(
|
||||
(config) => config.module == 'POS',
|
||||
);
|
||||
Object.assign(item['user_privilege'], {
|
||||
user_privilege_configurations: user_privilege_configurations,
|
||||
});
|
||||
if (item.role != 'superadmin') {
|
||||
const user_privilege_configurations = item[
|
||||
'user_privilege'
|
||||
]?.user_privilege_configurations?.filter(
|
||||
(config) => config.module == 'POS',
|
||||
);
|
||||
Object.assign(item['user_privilege'], {
|
||||
user_privilege_configurations: user_privilege_configurations,
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
|
@ -86,3 +89,52 @@ export class UserUpdatedHandler
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventsHandler(UserPrivilegeConfigUpdatedEvent)
|
||||
export class UserPrivilegeUpdateHandler
|
||||
implements IEventHandler<UserPrivilegeConfigUpdatedEvent>
|
||||
{
|
||||
constructor(
|
||||
private couchService: CouchService,
|
||||
private userService: UserDataService,
|
||||
) {}
|
||||
|
||||
async handle(event: UserPrivilegeConfigUpdatedEvent) {
|
||||
const data = event.data.data;
|
||||
|
||||
const users = await this.userService
|
||||
.getManyByOptions({
|
||||
where: {
|
||||
user_privilege_id: data.user_privilege_id ?? data.id,
|
||||
status: STATUS.ACTIVE,
|
||||
},
|
||||
relations: [
|
||||
'user_privilege',
|
||||
'user_privilege.user_privilege_configurations',
|
||||
],
|
||||
})
|
||||
.then((items) => {
|
||||
return items?.map((item) => {
|
||||
const user_privilege_configurations = item[
|
||||
'user_privilege'
|
||||
]?.user_privilege_configurations?.filter(
|
||||
(config) => config.module == 'POS',
|
||||
);
|
||||
Object.assign(item['user_privilege'], {
|
||||
user_privilege_configurations: user_privilege_configurations,
|
||||
});
|
||||
return item;
|
||||
});
|
||||
});
|
||||
|
||||
for (const user of users) {
|
||||
await this.couchService.updateDoc(
|
||||
{
|
||||
_id: user.id,
|
||||
...user,
|
||||
},
|
||||
'user',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export enum InvoiceType {
|
||||
BOOKING_INVOICE = 'this is your invoice',
|
||||
PAYMENT_CONFIRMATION = 'payment confirmation',
|
||||
INVOICE_EXPIRED = 'invoice has expired',
|
||||
REFUND_REQUEST = 'your refund request',
|
||||
REFUND_CONFIRMATION = 'refund confirmation',
|
||||
BOOKING_DATE_CHANGE = 'booking date change',
|
||||
}
|
||||
|
||||
export const PhoneNumber = '088';
|
|
@ -0,0 +1,55 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseCustomManager } from 'src/core/modules/domain/usecase/managers/base-custom.manager';
|
||||
import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity';
|
||||
import { EventTopics } from 'src/core/strings/constants/interface.constants';
|
||||
import { STATUS } from 'src/core/strings/constants/base.constants';
|
||||
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
|
||||
import { GeneratePdf } from '../templates/helpers/generate-pdf.helper';
|
||||
|
||||
@Injectable()
|
||||
export class PdfMakeManager extends BaseCustomManager<TransactionEntity> {
|
||||
get entityTarget(): any {
|
||||
return TransactionModel;
|
||||
}
|
||||
|
||||
get eventTopics(): EventTopics[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async validateProcess(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async beforeProcess(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async process(): Promise<void> {
|
||||
try {
|
||||
const transaction = await this.dataService.getOneByOptions({
|
||||
where: {
|
||||
id: this.data.id,
|
||||
},
|
||||
relations: ['items'],
|
||||
});
|
||||
const banks = await this.dataServiceFirstOpt.getManyByOptions({
|
||||
where: {
|
||||
status: STATUS.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
this.result = GeneratePdf(transaction, this.data.invoice_type, banks);
|
||||
} catch (error) {
|
||||
console.log(error, 'generate pdf');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async afterProcess(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
getResult() {
|
||||
return this.result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { InvoiceTemplate } from '../invoice.template';
|
||||
import * as PdfPrinter from 'pdfmake';
|
||||
import { PassThrough } from 'stream';
|
||||
|
||||
export async function GeneratePdf(transaction, invoiceType, banks) {
|
||||
const fonts = {
|
||||
Roboto: {
|
||||
normal: './assets/fonts/Roboto-Regular.ttf',
|
||||
bold: './assets/fonts/Roboto-Medium.ttf',
|
||||
italics: './assets/fonts/Roboto-Italic.ttf',
|
||||
bolditalics: './assets/fonts/Roboto-MediumItalic.ttf',
|
||||
},
|
||||
};
|
||||
|
||||
const printer = new PdfPrinter(fonts);
|
||||
|
||||
const docDefinition = InvoiceTemplate(transaction, invoiceType, banks);
|
||||
|
||||
const createPdfBuffer = (docDefinition) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pdfDoc = printer.createPdfKitDocument(docDefinition);
|
||||
const chunks = [];
|
||||
const stream = new PassThrough();
|
||||
|
||||
pdfDoc.pipe(stream);
|
||||
pdfDoc.end();
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
const pdfBuffer = Buffer.concat(chunks);
|
||||
resolve(pdfBuffer);
|
||||
});
|
||||
|
||||
stream.on('error', reject);
|
||||
});
|
||||
};
|
||||
|
||||
const pdfBuffer = await createPdfBuffer(docDefinition);
|
||||
return pdfBuffer;
|
||||
}
|
|
@ -0,0 +1,443 @@
|
|||
import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity';
|
||||
import { InvoiceType, PhoneNumber } from '../../../constants';
|
||||
import { PaymentMethodEntity } from 'src/modules/transaction/payment-method/domain/entities/payment-method.entity';
|
||||
|
||||
export function mappingHeader(transaction: TransactionEntity, invoiceType) {
|
||||
return [
|
||||
{
|
||||
stack: [
|
||||
{
|
||||
alignment: 'center',
|
||||
columns: [
|
||||
{
|
||||
width: 100,
|
||||
image: 'data',
|
||||
},
|
||||
{
|
||||
width: 'auto',
|
||||
text: 'have',
|
||||
fontSize: 50,
|
||||
bold: true,
|
||||
color: '#169f54',
|
||||
padding: [0, 0, 0, 0],
|
||||
margin: [0, 0, 0, 0],
|
||||
alignment: 'center',
|
||||
},
|
||||
{
|
||||
width: 'auto',
|
||||
text: 'fun',
|
||||
fontSize: 50,
|
||||
bold: true,
|
||||
color: '#61c4eb',
|
||||
padding: [0, 0, 0, 0],
|
||||
margin: [0, 0, 0, 0],
|
||||
// alignment: "center"
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: invoiceType.toUpperCase(),
|
||||
fontSize: 30,
|
||||
alignment: 'center',
|
||||
},
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
text: transaction.invoice_code,
|
||||
},
|
||||
{
|
||||
text: new Date(transaction.booking_date).toDateString(),
|
||||
alignment: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
width: 150,
|
||||
text: 'Jl, Kolonel Masturi No.KM. 11, \n Kertawangi, Kec. Cisatua, \n Kab. Bandung Barat, \n Jawa Barat 40551 \n 0815-6380-8021',
|
||||
// color: '#aaaaab',
|
||||
// bold: true,
|
||||
fontSize: 11,
|
||||
margin: [0, 20, 0, 5],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function mappingFooter() {
|
||||
return [
|
||||
[
|
||||
{
|
||||
text: 'we commits to prividing an educative, \n playful, and purposeful environment\n this is advantageous for all ages',
|
||||
border: [false, true, false, false],
|
||||
margin: [0, 5, 0, 5],
|
||||
alignment: 'right',
|
||||
},
|
||||
{
|
||||
text: 'Thank \n You',
|
||||
border: [false, true, false, false],
|
||||
fontSize: 20,
|
||||
bold: true,
|
||||
color: '#169f54',
|
||||
margin: [0, 5, 0, 5],
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
export function mappingPrice(
|
||||
transaction: TransactionEntity,
|
||||
invoiceType: InvoiceType,
|
||||
) {
|
||||
const result = [];
|
||||
const totalData = [
|
||||
InvoiceType.REFUND_CONFIRMATION,
|
||||
InvoiceType.REFUND_REQUEST,
|
||||
].includes(invoiceType)
|
||||
? transaction['refund'].refund_total
|
||||
: Number(transaction.payment_total).toLocaleString('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
});
|
||||
const subTotalData = [
|
||||
InvoiceType.REFUND_CONFIRMATION,
|
||||
InvoiceType.REFUND_REQUEST,
|
||||
].includes(invoiceType)
|
||||
? transaction['refund'].refund_total
|
||||
: Number(transaction.payment_sub_total).toLocaleString('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
});
|
||||
|
||||
const sub_total = [
|
||||
{
|
||||
text: 'SUBTOTAL',
|
||||
alignment: 'right',
|
||||
bold: true,
|
||||
margin: [0, 5, 0, 5],
|
||||
},
|
||||
{
|
||||
text: subTotalData,
|
||||
margin: [0, 5, 0, 5],
|
||||
},
|
||||
];
|
||||
result.push(sub_total);
|
||||
|
||||
if (Number(transaction.payment_discount_total ?? 0) > 0) {
|
||||
const discount = [
|
||||
{
|
||||
text: 'DISCOUNT',
|
||||
bold: true,
|
||||
alignment: 'right',
|
||||
margin: [0, 5, 0, 5],
|
||||
},
|
||||
{
|
||||
text: Number(transaction.payment_discount_total).toLocaleString(
|
||||
'id-ID',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
},
|
||||
),
|
||||
margin: [0, 5, 0, 5],
|
||||
},
|
||||
];
|
||||
result.push(discount);
|
||||
}
|
||||
|
||||
const total = [
|
||||
{
|
||||
text: 'TOTAL',
|
||||
bold: true,
|
||||
border: [false, false, false, true],
|
||||
alignment: 'right',
|
||||
margin: [0, 5, 0, 5],
|
||||
},
|
||||
{
|
||||
text: totalData,
|
||||
border: [false, false, false, true],
|
||||
margin: [0, 5, 0, 5],
|
||||
},
|
||||
];
|
||||
result.push(total);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mappingItem(transaction, invoiceType: InvoiceType) {
|
||||
const header = [
|
||||
{
|
||||
text: 'ITEM',
|
||||
border: [false, true, false, false],
|
||||
margin: [0, 5, 0, 5],
|
||||
bold: true,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
{
|
||||
text: 'QTY',
|
||||
alignment: 'center',
|
||||
border: [false, true, false, false],
|
||||
margin: [0, 5, 0, 5],
|
||||
bold: true,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
{
|
||||
text: 'PRICE',
|
||||
alignment: 'center',
|
||||
border: [false, true, false, false],
|
||||
margin: [0, 5, 0, 5],
|
||||
bold: true,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
{
|
||||
text: 'AMOUNT',
|
||||
border: [false, true, false, false],
|
||||
alignment: 'center',
|
||||
bold: true,
|
||||
margin: [0, 5, 0, 5],
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
];
|
||||
|
||||
const result = [];
|
||||
|
||||
if (
|
||||
[InvoiceType.REFUND_CONFIRMATION, InvoiceType.REFUND_REQUEST].includes(
|
||||
invoiceType,
|
||||
)
|
||||
) {
|
||||
transaction.refund?.refund_items
|
||||
?.filter((item) => Number(item.qty_refund) > 0)
|
||||
.forEach((item) => {
|
||||
const dataRow = [
|
||||
{
|
||||
text: item.transaction_item.item_name,
|
||||
margin: [0, 5, 0, 5],
|
||||
alignment: 'left',
|
||||
},
|
||||
{
|
||||
text: item.qty_refund,
|
||||
margin: [0, 5, 0, 5],
|
||||
alignment: 'center',
|
||||
},
|
||||
{
|
||||
text: Number(item.transaction_item.total_price).toLocaleString(
|
||||
'id-ID',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
},
|
||||
),
|
||||
margin: [0, 5, 0, 5],
|
||||
alignment: 'right',
|
||||
},
|
||||
{
|
||||
text: Number(item.refund_total).toLocaleString('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
}),
|
||||
alignment: 'right',
|
||||
margin: [0, 5, 0, 5],
|
||||
},
|
||||
];
|
||||
result.push(dataRow);
|
||||
});
|
||||
} else {
|
||||
transaction.items.forEach((item) => {
|
||||
const dataRow = [
|
||||
{
|
||||
text: item.item_name,
|
||||
margin: [0, 5, 0, 5],
|
||||
alignment: 'left',
|
||||
},
|
||||
{
|
||||
text: item.qty,
|
||||
margin: [0, 5, 0, 5],
|
||||
alignment: 'center',
|
||||
},
|
||||
{
|
||||
text: Number(item.item_price).toLocaleString('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
}),
|
||||
margin: [0, 5, 0, 5],
|
||||
alignment: 'right',
|
||||
},
|
||||
{
|
||||
text: Number(item.total_price).toLocaleString('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
}),
|
||||
alignment: 'right',
|
||||
margin: [0, 5, 0, 5],
|
||||
},
|
||||
];
|
||||
result.push(dataRow);
|
||||
});
|
||||
}
|
||||
|
||||
const body = [header, ...result];
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
export function mappingBody(
|
||||
transaction: TransactionEntity,
|
||||
invoiceType: InvoiceType,
|
||||
) {
|
||||
// booking date change information
|
||||
if (invoiceType == InvoiceType.BOOKING_DATE_CHANGE) {
|
||||
return [
|
||||
"Great news! We've successfully updated your booking date as per your request. \n We're exited to accommodate your new plans and ensure everything goes smoothly \n\n",
|
||||
'Here are your updated booking details:',
|
||||
{
|
||||
text: `\n\n Original Booking Date: ${new Date(
|
||||
transaction['booking_date_before'],
|
||||
).toDateString()} \n New Booking Date: ${new Date(
|
||||
transaction.booking_date,
|
||||
).toDateString()} \n\n`,
|
||||
bold: true,
|
||||
},
|
||||
"Here's a quick recap of your order :",
|
||||
];
|
||||
}
|
||||
|
||||
// booking invoice
|
||||
else if (invoiceType == InvoiceType.BOOKING_INVOICE) {
|
||||
return [
|
||||
"Thank you for choosing us! We're absolutely thrilled and can't wait to embark on this exiting day with you. See you soon for fun times ahead!",
|
||||
];
|
||||
} else if (invoiceType == InvoiceType.PAYMENT_CONFIRMATION) {
|
||||
// booking payment confirmation
|
||||
return [
|
||||
'We are exited to inform you that your payment has been successfully received! \n',
|
||||
'Attached to this email, you will find your confirmation receipt. \n',
|
||||
'Please keep this safe as you will need to show it at the entrance upon your arrival. \n',
|
||||
"It's your golden ticket to all the fun and excitement awaiting you \n\n",
|
||||
"Here's a quick recap: \n",
|
||||
];
|
||||
}
|
||||
|
||||
// expired information invoice
|
||||
// else if (invoiceType == InvoiceType.INVOICE_EXPIRED) {
|
||||
|
||||
// return [
|
||||
// "We hope this message finds you well!",
|
||||
// "Uh-oh! it looks like your invoice, dated 15 Juli, has officially expired as of 15 Juli. But no worries, we can fix this together \n",
|
||||
// "To keep the goof times rolling, our friendly support team is just a call away at \n",
|
||||
// "0564645 \n\n",
|
||||
// "Here are the detail of the expired invoice: "
|
||||
|
||||
// ]
|
||||
// }
|
||||
|
||||
// refund information
|
||||
else if (invoiceType == InvoiceType.REFUND_REQUEST) {
|
||||
return [
|
||||
"We'ew trully sorry for any inconvenience that led to this request. \n",
|
||||
"We've received your refund request for: \n",
|
||||
];
|
||||
}
|
||||
|
||||
// refund confirmation
|
||||
else if (invoiceType == InvoiceType.REFUND_CONFIRMATION) {
|
||||
return [
|
||||
'Good news! \n',
|
||||
"We've successfully processed your refund for: \n",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export function mappingBodyBottom(
|
||||
transaction: TransactionEntity,
|
||||
invoiceType: InvoiceType,
|
||||
banks: PaymentMethodEntity[],
|
||||
) {
|
||||
if (invoiceType == InvoiceType.BOOKING_DATE_CHANGE) {
|
||||
// booking date change information
|
||||
return [
|
||||
"For your convenience, we've attached a new confirmation receipt reflecting these changes \n",
|
||||
'Please be sure to bring this updated receipt with you on the new date \n\n',
|
||||
'If you have any questions or need further assistance, our friendly support team is just a call away at \n\n',
|
||||
PhoneNumber,
|
||||
"\nThank you and we can't wait to see you and make sure you hove an amazing time!",
|
||||
];
|
||||
} else if (invoiceType == InvoiceType.BOOKING_INVOICE) {
|
||||
// booking invoice
|
||||
return [
|
||||
'Just a friendly reminder that your invoice will expire on 24 Agustus 2024 \n',
|
||||
'To keep things running smoothly, please ensure your payment is completed before this data. \n',
|
||||
'\nFor youe convenience, here is a list of our account details \n\n',
|
||||
{
|
||||
text: [
|
||||
banks.forEach((bank) => {
|
||||
return {
|
||||
text: `${bank.issuer_name} ${bank.account_number} a/n ${bank.account_name} \n`,
|
||||
bold: true,
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
"\n Once you've made the payment, please kindly email or send the proof of payment so we can proceed with your booking promptly\n",
|
||||
'If you have any questions or need assistance, feel free to reach out to our support team at\n',
|
||||
PhoneNumber,
|
||||
];
|
||||
}
|
||||
|
||||
// booking payment confirmation
|
||||
else if (invoiceType == InvoiceType.PAYMENT_CONFIRMATION) {
|
||||
return [
|
||||
'If you have any questions or need assistance, feel free to reach out to our support team at\n',
|
||||
PhoneNumber,
|
||||
"\nDon't forget to bring a smile and your confirmation receipt (attached) for a smooth entry. \n",
|
||||
"We can't wait to see you and ensure you have an amazing time with us!",
|
||||
];
|
||||
}
|
||||
|
||||
// expired information invoice
|
||||
else if (invoiceType == InvoiceType.INVOICE_EXPIRED) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// refund information
|
||||
else if (invoiceType == InvoiceType.REFUND_REQUEST) {
|
||||
return [
|
||||
"Your satisfaction is important to us, and we're commited to resolving this as quickly as possible.\n",
|
||||
'Our team is already on it and will process your refund request promptly. \n',
|
||||
"We'll keep you updated and notify you once the refund has been processed. \n",
|
||||
"If you have any questions or need futher assistance, don't hestitate to reach out us at \n",
|
||||
PhoneNumber,
|
||||
'\n\n',
|
||||
'Thank you for your patience and understanding \n',
|
||||
'We appreciate your feedback and here to make things right!',
|
||||
];
|
||||
}
|
||||
|
||||
// refund confirmation
|
||||
else if (invoiceType == InvoiceType.REFUND_CONFIRMATION) {
|
||||
return [
|
||||
'Here are the details of your refund: \n\n',
|
||||
{
|
||||
text: `Transaction Number: ${transaction.invoice_code} \n`,
|
||||
},
|
||||
{
|
||||
text: `Refund Processed: ${transaction['refund'].refund_date}\n`,
|
||||
},
|
||||
{
|
||||
text: `Bank Account: ${transaction['refund'].bank_name} \n`,
|
||||
},
|
||||
{
|
||||
text: `Account Number: ${transaction['refund'].bank_account_number} \n`,
|
||||
},
|
||||
{
|
||||
text: `Account Name: ${transaction['refund'].bank_account_name} \n`,
|
||||
},
|
||||
"\n We hope this helps make things righ, and we're he to assist if you need anything else. \n",
|
||||
'You should see the refund reflected in your account within 3 Business days \n\n',
|
||||
'Thank you for your patience and understanding\n',
|
||||
'If you have any questions or need assistance, feel free to reach out to our support team at \n',
|
||||
PhoneNumber,
|
||||
"\nWe're alyways here to help!",
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity';
|
||||
import {
|
||||
mappingBody,
|
||||
mappingBodyBottom,
|
||||
mappingFooter,
|
||||
mappingHeader,
|
||||
mappingItem,
|
||||
mappingPrice,
|
||||
} from './helpers/invoice-mapping.helper';
|
||||
import { InvoiceType } from '../../constants';
|
||||
import { PaymentMethodEntity } from 'src/modules/transaction/payment-method/domain/entities/payment-method.entity';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export function InvoiceTemplate(
|
||||
transaction: TransactionEntity,
|
||||
invoiceType: InvoiceType,
|
||||
banks: PaymentMethodEntity[],
|
||||
) {
|
||||
const filePath = './assets/image/logo.jpeg';
|
||||
const imageBase64 = fs.readFileSync(filePath).toString('base64');
|
||||
const imageUrl = `data:image/png;base64,${imageBase64}`;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
alignment: 'justify',
|
||||
columns: mappingHeader(transaction, invoiceType),
|
||||
},
|
||||
|
||||
'\n\n',
|
||||
|
||||
// tipe booking date change tidak ada
|
||||
{
|
||||
columns: [
|
||||
{
|
||||
text: `Dear, \n ${transaction.customer_name} \n ${
|
||||
transaction.customer_phone
|
||||
} \n Booking Date: ${new Date(
|
||||
transaction.booking_date,
|
||||
).toDateString()}`,
|
||||
bold: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
'\n',
|
||||
{
|
||||
text: mappingBody(transaction, invoiceType),
|
||||
},
|
||||
'\n',
|
||||
|
||||
{
|
||||
layout: {
|
||||
defaultBorder: false,
|
||||
hLineWidth: function () {
|
||||
return 1;
|
||||
},
|
||||
vLineWidth: function () {
|
||||
return 1;
|
||||
},
|
||||
hLineColor: function (i) {
|
||||
return i === 0 ? '#000000' : '#eaeaea';
|
||||
},
|
||||
vLineColor: function () {
|
||||
return '#eaeaea';
|
||||
},
|
||||
hLineStyle: function () {
|
||||
// if (i === 0 || i === node.table.body.length) {
|
||||
return null;
|
||||
//}
|
||||
},
|
||||
// vLineStyle: function (i, node) { return {dash: { length: 10, space: 4 }}; },
|
||||
paddingLeft: function () {
|
||||
return 10;
|
||||
},
|
||||
paddingRight: function () {
|
||||
return 10;
|
||||
},
|
||||
paddingTop: function () {
|
||||
return 3;
|
||||
},
|
||||
paddingBottom: function () {
|
||||
return 3;
|
||||
},
|
||||
fillColor: function () {
|
||||
return '#fff';
|
||||
},
|
||||
},
|
||||
table: {
|
||||
widths: ['*', 'auto', 'auto', 100],
|
||||
body: mappingItem(transaction, invoiceType),
|
||||
},
|
||||
},
|
||||
'\n',
|
||||
{
|
||||
layout: {
|
||||
defaultBorder: false,
|
||||
hLineWidth: function () {
|
||||
return 1;
|
||||
},
|
||||
vLineWidth: function () {
|
||||
return 1;
|
||||
},
|
||||
hLineColor: function (i) {
|
||||
return i === 3 ? '#000000' : '#eaeaea';
|
||||
},
|
||||
vLineColor: function () {
|
||||
return '#eaeaea';
|
||||
},
|
||||
hLineStyle: function () {
|
||||
// if (i === 0 || i === node.table.body.length) {
|
||||
return null;
|
||||
//}
|
||||
},
|
||||
// vLineStyle: function () { return {dash: { length: 10, space: 4 }}; },
|
||||
paddingLeft: function () {
|
||||
return 10;
|
||||
},
|
||||
paddingRight: function () {
|
||||
return 10;
|
||||
},
|
||||
paddingTop: function () {
|
||||
return 3;
|
||||
},
|
||||
paddingBottom: function (i) {
|
||||
return i === 2 ? 20 : 3;
|
||||
},
|
||||
fillColor: function () {
|
||||
return '#fff';
|
||||
},
|
||||
},
|
||||
table: {
|
||||
headerRows: 1,
|
||||
widths: ['*', 100],
|
||||
body: mappingPrice(transaction, invoiceType),
|
||||
},
|
||||
},
|
||||
|
||||
'\n',
|
||||
{
|
||||
text: mappingBodyBottom(transaction, invoiceType, banks),
|
||||
},
|
||||
'\n',
|
||||
|
||||
{
|
||||
layout: {
|
||||
defaultBorder: false,
|
||||
hLineWidth: function () {
|
||||
return 1;
|
||||
},
|
||||
vLineWidth: function () {
|
||||
return 1;
|
||||
},
|
||||
hLineColor: function (i) {
|
||||
return i === 0 ? '#000000' : '#eaeaea';
|
||||
},
|
||||
vLineColor: function () {
|
||||
return '#eaeaea';
|
||||
},
|
||||
hLineStyle: function () {
|
||||
// if (i === 0 || i === node.table.body.length) {
|
||||
return null;
|
||||
//}
|
||||
},
|
||||
// vLineStyle: function () { return {dash: { length: 10, space: 4 }}; },
|
||||
paddingLeft: function () {
|
||||
return 10;
|
||||
},
|
||||
paddingRight: function () {
|
||||
return 10;
|
||||
},
|
||||
paddingTop: function () {
|
||||
return 20;
|
||||
},
|
||||
paddingBottom: function () {
|
||||
return 3;
|
||||
},
|
||||
fillColor: function () {
|
||||
return '#fff';
|
||||
},
|
||||
},
|
||||
table: {
|
||||
widths: ['*', 200],
|
||||
body: mappingFooter(),
|
||||
},
|
||||
},
|
||||
],
|
||||
styles: {
|
||||
notesTitle: {
|
||||
fontSize: 10,
|
||||
bold: true,
|
||||
margin: [0, 50, 0, 3],
|
||||
},
|
||||
notesText: {
|
||||
fontSize: 10,
|
||||
},
|
||||
tableExample: {
|
||||
margin: [0, 5, 0, 15],
|
||||
headerRows: 1,
|
||||
widths: ['*', 100],
|
||||
},
|
||||
tableHeader: {
|
||||
bold: true,
|
||||
fontSize: 13,
|
||||
color: 'black',
|
||||
},
|
||||
},
|
||||
defaultStyle: {
|
||||
columnGap: 20,
|
||||
},
|
||||
images: {
|
||||
data: imageUrl,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { PdfMakeManager } from './domain/managers/pdf-make.manager';
|
||||
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
|
||||
import { PaymentMethodDataService } from 'src/modules/transaction/payment-method/data/services/payment-method-data.service';
|
||||
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
|
||||
import { TransactionItemModel } from 'src/modules/transaction/transaction/data/models/transaction-item.model';
|
||||
import { PaymentMethodModel } from 'src/modules/transaction/payment-method/data/models/payment-method.model';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { TransactionTaxModel } from 'src/modules/transaction/transaction/data/models/transaction-tax.model';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot(),
|
||||
TypeOrmModule.forFeature([TransactionModel], CONNECTION_NAME.DEFAULT),
|
||||
CqrsModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [PdfMakeManager],
|
||||
})
|
||||
export class ExportModule {}
|
|
@ -0,0 +1,16 @@
|
|||
import { Controller, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Public } from 'src/core/guards';
|
||||
|
||||
@ApiTags('export')
|
||||
@Controller('export')
|
||||
@Public(true)
|
||||
export class ExportController {
|
||||
constructor() {}
|
||||
|
||||
// @Post('pdf/example')
|
||||
// async exportPdf(
|
||||
// ): Promise<any> {
|
||||
// return PdfMaker();
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import { google } from 'googleapis';
|
||||
import * as fs from 'fs';
|
||||
import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity';
|
||||
|
||||
export async function CreateEventCalendarHelper(
|
||||
transaction: TransactionEntity,
|
||||
isDelete = false,
|
||||
) {
|
||||
let result;
|
||||
|
||||
const filePath = './assets/json/google-credential.json';
|
||||
const credential = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
|
||||
const client = new google.auth.JWT({
|
||||
email: credential.client_email,
|
||||
key: credential.private_key,
|
||||
scopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
});
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: 'v3',
|
||||
auth: client,
|
||||
});
|
||||
|
||||
const eventData = mappingData(transaction);
|
||||
|
||||
if (transaction.calendar_id) {
|
||||
result = await calendar.events.update(
|
||||
{
|
||||
calendarId: process.env.GOOGLE_CALENDAR_ID,
|
||||
eventId: transaction.calendar_id,
|
||||
requestBody: eventData,
|
||||
},
|
||||
{},
|
||||
);
|
||||
} else if (!isDelete) {
|
||||
result = await calendar.events.insert(
|
||||
{
|
||||
calendarId: process.env.GOOGLE_CALENDAR_ID,
|
||||
requestBody: eventData,
|
||||
},
|
||||
{},
|
||||
);
|
||||
} else {
|
||||
result = await calendar.events.delete(
|
||||
{
|
||||
calendarId: process.env.GOOGLE_CALENDAR_ID,
|
||||
eventId: transaction.calendar_id,
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
return result?.data;
|
||||
}
|
||||
|
||||
function mappingData(transaction) {
|
||||
return {
|
||||
summary: transaction.customer_name ?? transaction.invoice_code,
|
||||
description: `<b>Booking for invoice ${
|
||||
transaction.invoice_code
|
||||
}</b><p>List Items :</p><ul>${transaction.items.map(
|
||||
(item) => `<li>${item.item_name}</li>`,
|
||||
)}</ul>`,
|
||||
start: {
|
||||
dateTime: new Date(transaction.booking_date).toISOString(),
|
||||
timeZone: 'Asia/Jakarta',
|
||||
},
|
||||
end: {
|
||||
dateTime: new Date(transaction.booking_date).toISOString(),
|
||||
timeZone: 'Asia/Jakarta',
|
||||
},
|
||||
reminders: {
|
||||
useDefault: false,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -8,7 +8,7 @@ export class IndexHolidayCalendarManager {
|
|||
const events = [];
|
||||
const calendar = google.calendar({
|
||||
version: 'v3',
|
||||
auth: 'AIzaSyCsCt6PDd6uYLkahvtdvCoMWf-1_QaLiNM',
|
||||
auth: process.env.GOOGLE_CALENDAR_KEY,
|
||||
});
|
||||
const calendarId = 'id.indonesian#holiday@group.v.calendar.google.com';
|
||||
|
||||
|
|
|
@ -4,10 +4,29 @@ import { CqrsModule } from '@nestjs/cqrs';
|
|||
import { IndexHolidayCalendarManager } from '../../configuration/google-calendar/domain/usecases/managers/index-holiday-google-calendar.manager';
|
||||
import { GoogleCalendarController } from './infrastructure/google-calendar.controller';
|
||||
import { GoogleCalendarOrchestrator } from './domain/usecases/google-calendar.orchestrator';
|
||||
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
|
||||
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
|
||||
import { TransactionItemModel } from 'src/modules/transaction/transaction/data/models/transaction-item.model';
|
||||
import { TransactionTaxModel } from 'src/modules/transaction/transaction/data/models/transaction-tax.model';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forRoot(), CqrsModule],
|
||||
imports: [
|
||||
ConfigModule.forRoot(),
|
||||
TypeOrmModule.forFeature(
|
||||
[TransactionModel, TransactionItemModel, TransactionTaxModel],
|
||||
CONNECTION_NAME.DEFAULT,
|
||||
),
|
||||
CqrsModule,
|
||||
],
|
||||
controllers: [GoogleCalendarController],
|
||||
providers: [IndexHolidayCalendarManager, GoogleCalendarOrchestrator],
|
||||
providers: [
|
||||
IndexHolidayCalendarManager,
|
||||
|
||||
TransactionDataService,
|
||||
|
||||
GoogleCalendarOrchestrator,
|
||||
],
|
||||
})
|
||||
export class GoogleCalendarModule {}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GoogleCalendarOrchestrator } from './../domain/usecases/google-calendar.orchestrator';
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Public } from 'src/core/guards';
|
||||
import { FilterGoogleCalendarDto } from './dto/filter-google-calendar.dto';
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
import { PosLogEntity, PosLogType } from '../../domain/entities/pos-log.entity';
|
||||
import { BaseCoreModel } from 'src/core/modules/data/model/base-core.model';
|
||||
|
||||
@Entity(TABLE_NAME.LOG_POS)
|
||||
export class PosLogModel
|
||||
extends BaseCoreModel<PosLogEntity>
|
||||
implements PosLogEntity
|
||||
{
|
||||
@Column('varchar', { name: 'type', default: PosLogType.cash_witdrawal })
|
||||
type: PosLogType;
|
||||
|
||||
@Column('bigint', { name: 'pos_number', nullable: true })
|
||||
pos_number: number;
|
||||
|
||||
@Column('decimal', { name: 'total_balance', nullable: true })
|
||||
total_balance: number;
|
||||
|
||||
@Column('bigint', { name: 'created_at', nullable: true })
|
||||
created_at: number;
|
||||
|
||||
@Column('varchar', { name: 'creator_name', nullable: true })
|
||||
creator_name: string;
|
||||
|
||||
@Column('varchar', { name: 'creator_id', nullable: true })
|
||||
creator_id: string;
|
||||
|
||||
@Column('varchar', { name: 'drawn_by_name', nullable: true })
|
||||
drawn_by_name: string;
|
||||
|
||||
@Column('varchar', { name: 'drawn_by_id', nullable: true })
|
||||
drawn_by_id: string;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseDataService } from 'src/core/modules/data/service/base-data.service';
|
||||
import { PosLogEntity } from '../../domain/entities/pos-log.entity';
|
||||
import { PosLogModel } from '../models/pos-log.model';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class PosLogService extends BaseDataService<PosLogEntity> {
|
||||
constructor(
|
||||
@InjectRepository(PosLogModel, CONNECTION_NAME.DEFAULT)
|
||||
private repo: Repository<PosLogModel>,
|
||||
) {
|
||||
super(repo);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { BaseCoreEntity } from 'src/core/modules/domain/entities/base-core.entity';
|
||||
|
||||
export interface PosLogEntity extends BaseCoreEntity {
|
||||
type: PosLogType;
|
||||
pos_number: number;
|
||||
total_balance: number;
|
||||
created_at: number;
|
||||
creator_name: string;
|
||||
creator_id: string;
|
||||
}
|
||||
|
||||
export enum PosLogType {
|
||||
cash_witdrawal = 'cash withdrawal',
|
||||
opening_cash = 'opening_cash',
|
||||
login = 'login',
|
||||
logout = 'logout',
|
||||
}
|
|
@ -1,9 +1,33 @@
|
|||
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
|
||||
import { RecordLog } from '../entities/log.event';
|
||||
import { LogService } from '../../data/services/log.service';
|
||||
import { LogModel } from '../../data/models/log.model';
|
||||
|
||||
@EventsHandler(RecordLog)
|
||||
export class RecordLogHandler implements IEventHandler<RecordLog> {
|
||||
constructor(private dataService: LogService) {}
|
||||
|
||||
async handle(event: RecordLog) {
|
||||
// TODO: Implement logic here
|
||||
const data = event.data;
|
||||
|
||||
const queryRunner = this.dataService
|
||||
.getRepository()
|
||||
.manager.connection.createQueryRunner();
|
||||
|
||||
const log = new LogModel();
|
||||
|
||||
Object.assign(log, {
|
||||
data_id: data.id,
|
||||
module: data.module,
|
||||
description: data.description,
|
||||
process: data.op,
|
||||
old_data: data.old,
|
||||
data: data.data,
|
||||
created_at: new Date().getTime(),
|
||||
creator_name: data.user.name,
|
||||
creator_id: data.user.id,
|
||||
});
|
||||
|
||||
await this.dataService.create(queryRunner, LogModel, log);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
|
||||
import { ChangeDocEvent } from 'src/modules/configuration/couch/domain/events/change-doc.event';
|
||||
import { PosLogService } from '../../data/services/pos-log.service';
|
||||
import { PosLogModel } from '../../data/models/pos-log.model';
|
||||
import { PosLogType } from '../entities/pos-log.entity';
|
||||
|
||||
@EventsHandler(ChangeDocEvent)
|
||||
export class RecordPosLogHandler implements IEventHandler<ChangeDocEvent> {
|
||||
constructor(private dataService: PosLogService) {}
|
||||
|
||||
async handle(event: ChangeDocEvent) {
|
||||
try {
|
||||
const database = event.data.database;
|
||||
const data = event.data.data;
|
||||
|
||||
if (!['pos_cash_activity', 'pos_activity'].includes(database)) return;
|
||||
|
||||
const queryRunner = this.dataService
|
||||
.getRepository()
|
||||
.manager.connection.createQueryRunner();
|
||||
|
||||
const activity = new PosLogModel();
|
||||
|
||||
Object.assign(activity, {
|
||||
id: data._id,
|
||||
type: PosLogType[data.type],
|
||||
total_balance: data.withdrawal_cash ?? data.opening_cash_balance,
|
||||
pos_number: data.pos_number,
|
||||
creator_id: data.pos_admin?.id,
|
||||
creator_name: data.pos_admin?.name ?? data.pos_admin?.username,
|
||||
drawn_by_id: data.withdraw_user?.id,
|
||||
drawn_by_name: data.withdraw_user?.name ?? data.withdraw_user?.username,
|
||||
created_at: data.created_at,
|
||||
});
|
||||
|
||||
await this.dataService.create(queryRunner, PosLogModel, activity);
|
||||
} catch (error) {
|
||||
console.log('error handling pos activity couch');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,12 +9,15 @@ import { RecordErrorLogHandler } from './domain/handlers/error-log.handler';
|
|||
import { RecordLogHandler } from './domain/handlers/log.handler';
|
||||
import { ErrorLogService } from './data/services/error-log.service';
|
||||
import { LogService } from './data/services/log.service';
|
||||
import { PosLogModel } from './data/models/pos-log.model';
|
||||
import { PosLogService } from './data/services/pos-log.service';
|
||||
import { RecordPosLogHandler } from './domain/handlers/pos-log.handler';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot(),
|
||||
TypeOrmModule.forFeature(
|
||||
[LogModel, ErrorLogModel],
|
||||
[LogModel, ErrorLogModel, PosLogModel],
|
||||
CONNECTION_NAME.DEFAULT,
|
||||
),
|
||||
CqrsModule,
|
||||
|
@ -22,9 +25,11 @@ import { LogService } from './data/services/log.service';
|
|||
controllers: [],
|
||||
providers: [
|
||||
RecordLogHandler,
|
||||
RecordPosLogHandler,
|
||||
RecordErrorLogHandler,
|
||||
|
||||
LogService,
|
||||
PosLogService,
|
||||
ErrorLogService,
|
||||
],
|
||||
})
|
||||
|
|
|
@ -4,9 +4,20 @@ import { PaymentMethodDataService } from 'src/modules/transaction/payment-method
|
|||
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';
|
||||
import { InvoiceType } from 'src/modules/configuration/export/constants';
|
||||
import { GeneratePdf } from 'src/modules/configuration/export/domain/templates/helpers/generate-pdf.helper';
|
||||
import { TransactionUpdatedEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-updated.event';
|
||||
import { RefundChangeStatusEvent } from 'src/modules/transaction/refund/domain/entities/event/refund-change-status.event';
|
||||
import { RefundCreatedEvent } from 'src/modules/transaction/refund/domain/entities/event/refund-created.event';
|
||||
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
@EventsHandler(TransactionChangeStatusEvent)
|
||||
@EventsHandler(
|
||||
TransactionChangeStatusEvent,
|
||||
TransactionUpdatedEvent,
|
||||
RefundChangeStatusEvent,
|
||||
RefundCreatedEvent,
|
||||
)
|
||||
export class PaymentTransactionHandler
|
||||
implements IEventHandler<TransactionChangeStatusEvent>
|
||||
{
|
||||
|
@ -16,46 +27,272 @@ export class PaymentTransactionHandler
|
|||
) {}
|
||||
|
||||
async handle(event: TransactionChangeStatusEvent) {
|
||||
const data_id = event.data.id;
|
||||
const old_data = event.data.old;
|
||||
const current_data = event.data.data;
|
||||
let payments = [];
|
||||
try {
|
||||
const old_data = event.data.old;
|
||||
const current_data = event.data.data;
|
||||
const data_id = current_data.transaction_id ?? event.data.id;
|
||||
const from_refund = event.data.module == TABLE_NAME.REFUND;
|
||||
console.log('payment handlet', { data_id });
|
||||
|
||||
if (
|
||||
old_data.status == STATUS.DRAFT &&
|
||||
current_data.status == STATUS.PENDING &&
|
||||
current_data.payment_type != TransactionPaymentType.COUNTER &&
|
||||
!!current_data.customer_email
|
||||
) {
|
||||
if (current_data.payment_type != TransactionPaymentType.MIDTRANS) {
|
||||
payments = await this.paymentService.getManyByOptions({
|
||||
where: {
|
||||
status: STATUS.ACTIVE,
|
||||
},
|
||||
});
|
||||
}
|
||||
const payments = await this.paymentService.getManyByOptions({
|
||||
where: {
|
||||
status: STATUS.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
const transaction = await this.dataService.getOneByOptions({
|
||||
let transaction = await this.dataService.getOneByOptions({
|
||||
where: {
|
||||
id: data_id,
|
||||
},
|
||||
relations: ['items'],
|
||||
relations: [
|
||||
'items',
|
||||
'refunds',
|
||||
'refunds.refund_items',
|
||||
'refunds.refund_items.transaction_item',
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
if (!transaction) {
|
||||
transaction = await this.dataService.getOneByOptions({
|
||||
where: {
|
||||
id: event.data.id,
|
||||
},
|
||||
relations: [
|
||||
'items',
|
||||
'refunds',
|
||||
'refunds.refund_items',
|
||||
'refunds.refund_items.transaction_item',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(transaction, {
|
||||
booking_date: new Date(transaction.booking_date).toDateString(),
|
||||
booking_date_before: new Date(
|
||||
transaction.booking_date_before,
|
||||
).toDateString(),
|
||||
email: transaction.customer_email,
|
||||
payment_methods: payments,
|
||||
});
|
||||
|
||||
const refund = transaction?.['refunds']?.find(
|
||||
(refund) => ![STATUS.CANCEL].includes(refund.status),
|
||||
);
|
||||
if (refund) {
|
||||
Object.assign(refund, {
|
||||
refund_date: new Date(refund?.refund_date).toDateString(),
|
||||
request_date: new Date(refund?.request_date).toDateString(),
|
||||
refund_total: Number(refund?.refund_total).toLocaleString('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
}),
|
||||
});
|
||||
|
||||
Object.assign(transaction, {
|
||||
refund: refund,
|
||||
});
|
||||
}
|
||||
|
||||
if (transaction?.['refund']?.refund_items.length > 0) {
|
||||
Object.assign(transaction, {
|
||||
refund_items: `
|
||||
<p>Refund Items:</p>
|
||||
<ul style="padding-left:15px">
|
||||
${transaction?.['refund']?.refund_items
|
||||
?.filter((item) => Number(item.qty_refund) > 0)
|
||||
.map((item) => {
|
||||
return `
|
||||
<li>${item.qty_refund} ${item.transaction_item.item_name} </li>
|
||||
`;
|
||||
})}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!transaction.customer_email) return;
|
||||
|
||||
// refund request
|
||||
if (
|
||||
from_refund &&
|
||||
transaction['refund'] &&
|
||||
[STATUS.DRAFT].includes(transaction['refund'].status)
|
||||
) {
|
||||
Logger.verbose('Send Refund Request', 'PaymentTransaction');
|
||||
const pdf = await GeneratePdf(
|
||||
transaction,
|
||||
InvoiceType.REFUND_REQUEST,
|
||||
payments,
|
||||
);
|
||||
sendEmail(
|
||||
[
|
||||
{
|
||||
...transaction,
|
||||
email: transaction.customer_email,
|
||||
payment_methods: payments,
|
||||
payment_total: Number(transaction.payment_total).toLocaleString(
|
||||
'id-ID',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
'Payment Confirmation',
|
||||
InvoiceType.REFUND_REQUEST,
|
||||
pdf,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// refund confirmation
|
||||
else if (
|
||||
from_refund &&
|
||||
transaction['refund'] &&
|
||||
transaction['refund'].status == STATUS.REFUNDED
|
||||
) {
|
||||
Logger.verbose('Send Refund Confirmation', 'PaymentTransaction');
|
||||
const pdf = await GeneratePdf(
|
||||
transaction,
|
||||
InvoiceType.REFUND_CONFIRMATION,
|
||||
payments,
|
||||
);
|
||||
sendEmail(
|
||||
[
|
||||
{
|
||||
...transaction,
|
||||
payment_total: Number(transaction.payment_total).toLocaleString(
|
||||
'id-ID',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
InvoiceType.REFUND_CONFIRMATION,
|
||||
pdf,
|
||||
);
|
||||
}
|
||||
|
||||
// payment settled
|
||||
else if (
|
||||
!from_refund &&
|
||||
old_data.status != current_data.status &&
|
||||
[STATUS.ACTIVE, STATUS.SETTLED].includes(current_data.status)
|
||||
) {
|
||||
Logger.verbose('Send Payment Settled', 'PaymentTransaction');
|
||||
const pdf = await GeneratePdf(
|
||||
transaction,
|
||||
InvoiceType.PAYMENT_CONFIRMATION,
|
||||
payments,
|
||||
);
|
||||
sendEmail(
|
||||
[
|
||||
{
|
||||
...transaction,
|
||||
payment_total: Number(transaction.payment_total).toLocaleString(
|
||||
'id-ID',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
InvoiceType.PAYMENT_CONFIRMATION,
|
||||
pdf,
|
||||
);
|
||||
}
|
||||
|
||||
// payment confirm to pending
|
||||
else if (
|
||||
!from_refund &&
|
||||
old_data.status != current_data.status &&
|
||||
[STATUS.PENDING].includes(current_data.status)
|
||||
) {
|
||||
Logger.verbose('Send Confirmation to Pending', 'PaymentTransaction');
|
||||
const pdf = await GeneratePdf(
|
||||
transaction,
|
||||
InvoiceType.BOOKING_INVOICE,
|
||||
payments,
|
||||
);
|
||||
sendEmail(
|
||||
[
|
||||
{
|
||||
...transaction,
|
||||
payment_total: Number(transaction.payment_total).toLocaleString(
|
||||
'id-ID',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
InvoiceType.BOOKING_INVOICE,
|
||||
pdf,
|
||||
);
|
||||
}
|
||||
// payment expired
|
||||
else if (
|
||||
!from_refund &&
|
||||
old_data.status != current_data.status &&
|
||||
[STATUS.PENDING, STATUS.EXPIRED].includes(current_data.status)
|
||||
) {
|
||||
Logger.verbose('Send Payment Expired', 'PaymentTransaction');
|
||||
const pdf = await GeneratePdf(
|
||||
transaction,
|
||||
InvoiceType.INVOICE_EXPIRED,
|
||||
payments,
|
||||
);
|
||||
sendEmail(
|
||||
[
|
||||
{
|
||||
...transaction,
|
||||
payment_total: Number(transaction.payment_total).toLocaleString(
|
||||
'id-ID',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
InvoiceType.INVOICE_EXPIRED,
|
||||
pdf,
|
||||
);
|
||||
}
|
||||
|
||||
// change booking date
|
||||
else if (
|
||||
!from_refund &&
|
||||
old_data.booking_date != current_data.booking_date &&
|
||||
[STATUS.SETTLED, STATUS.ACTIVE, STATUS.PENDING].includes(
|
||||
current_data.status,
|
||||
)
|
||||
) {
|
||||
Logger.verbose('Send Change Booking Date', 'PaymentTransaction');
|
||||
const pdf = await GeneratePdf(
|
||||
transaction,
|
||||
InvoiceType.BOOKING_DATE_CHANGE,
|
||||
payments,
|
||||
);
|
||||
sendEmail(
|
||||
[
|
||||
{
|
||||
...transaction,
|
||||
payment_total: Number(transaction.payment_total).toLocaleString(
|
||||
'id-ID',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
InvoiceType.BOOKING_DATE_CHANGE,
|
||||
pdf,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
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';
|
||||
import { InvoiceType } from 'src/modules/configuration/export/constants';
|
||||
|
||||
export async function sendEmail(receivers, subject) {
|
||||
export async function sendEmail(receivers, invoiceType, attachment?) {
|
||||
const smtpTransport = nodemailer.createTransport({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_POST,
|
||||
|
@ -13,38 +13,62 @@ export async function sendEmail(receivers, subject) {
|
|||
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';
|
||||
try {
|
||||
const templateName = getTemplate(receiver.payment_type, invoiceType);
|
||||
const templatePath = `./assets/email-template/${templateName}.html`;
|
||||
const templateSource = fs.readFileSync(templatePath, 'utf8');
|
||||
|
||||
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 template = handlebars.compile(templateSource);
|
||||
const htmlToSend = template(receiver);
|
||||
const emailContext = {
|
||||
from: process.env.EMAIL_SENDER ?? 'no-reply@weplayground.app',
|
||||
to: receiver.email,
|
||||
subject: invoiceType,
|
||||
html: htmlToSend,
|
||||
attachDataUrls: true,
|
||||
attachments: [
|
||||
{
|
||||
filename: `${invoiceType}.pdf`,
|
||||
content: attachment,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const emailContext = {
|
||||
from: 'no-reply@eigen.co.id',
|
||||
to: receiver.email,
|
||||
subject: subject,
|
||||
html: htmlToSend,
|
||||
attachDataUrls: true,
|
||||
};
|
||||
await new Promise((f) => setTimeout(f, 2000));
|
||||
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
smtpTransport.sendMail(emailContext, function (err) {
|
||||
if (err) {
|
||||
console.log(err, `Error occurs on send to ${receiver.email}`);
|
||||
} else {
|
||||
console.log(`Email sent to ${receiver.email}`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error, `Error occurs on send to ${receiver.email}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTemplate(transactionType, invoiceType) {
|
||||
if (invoiceType == InvoiceType.BOOKING_DATE_CHANGE) {
|
||||
return 'change-date-information';
|
||||
} else if (invoiceType == InvoiceType.INVOICE_EXPIRED) {
|
||||
return 'invoice-expired';
|
||||
} else if (invoiceType == InvoiceType.REFUND_REQUEST) {
|
||||
return 'refund-request';
|
||||
} else if (invoiceType == InvoiceType.REFUND_CONFIRMATION) {
|
||||
return 'refund-confirmation';
|
||||
} else if (invoiceType == InvoiceType.PAYMENT_CONFIRMATION) {
|
||||
return 'payment-confirmation';
|
||||
} else if (
|
||||
invoiceType == InvoiceType.BOOKING_INVOICE &&
|
||||
transactionType != TransactionPaymentType.MIDTRANS
|
||||
) {
|
||||
return 'invoice-bank';
|
||||
} else if (invoiceType == InvoiceType.BOOKING_INVOICE) {
|
||||
return 'invoice-midtrans';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import { Controller, Get, Injectable } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Public } from 'src/core/guards';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
@ApiTags(`email templates`)
|
||||
@Controller('v1/email-templates')
|
||||
@Public()
|
||||
@Injectable()
|
||||
export class MailTemplateController {
|
||||
getTemplate(templateName) {
|
||||
const templatePath = path.join(
|
||||
__dirname,
|
||||
'../../../../../',
|
||||
`src/modules/configuration/mail/domain/email-template/${templateName}.html`,
|
||||
);
|
||||
return fs.readFileSync(templatePath, 'utf8');
|
||||
}
|
||||
|
||||
@Get('date-change')
|
||||
async getDateChange() {
|
||||
return this.getTemplate('change-date');
|
||||
}
|
||||
|
||||
@Get('invoice')
|
||||
async getBookingInvoice() {
|
||||
return this.getTemplate('invoice');
|
||||
}
|
||||
|
||||
@Get('payment-confirmation-bank')
|
||||
async getPaymentConfirmation() {
|
||||
return this.getTemplate('payment-confirmation-bank');
|
||||
}
|
||||
|
||||
@Get('payment-confirmation-midtrans')
|
||||
async getPaymentConfirmationMidtrans() {
|
||||
return this.getTemplate('payment-confirmation-midtrans');
|
||||
}
|
||||
|
||||
@Get('invoice-expired')
|
||||
async getInvoiceExpired() {
|
||||
return this.getTemplate('invoice-expired');
|
||||
}
|
||||
|
||||
@Get('refund-confirmation')
|
||||
async getRefundConfirmation() {
|
||||
return this.getTemplate('refund-confirmation');
|
||||
}
|
||||
|
||||
@Get('refund-request')
|
||||
async getRefundRequest() {
|
||||
return this.getTemplate('refund-request');
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ 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';
|
||||
import { MailTemplateController } from './infrastructure/mail.controller';
|
||||
import { PdfMakeManager } from '../export/domain/managers/pdf-make.manager';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -18,8 +20,10 @@ import { PaymentTransactionHandler } from './domain/handlers/payment-transaction
|
|||
),
|
||||
CqrsModule,
|
||||
],
|
||||
controllers: [],
|
||||
controllers: [MailTemplateController],
|
||||
providers: [
|
||||
PdfMakeManager,
|
||||
|
||||
PaymentTransactionHandler,
|
||||
PaymentMethodDataService,
|
||||
TransactionDataService,
|
||||
|
|
|
@ -1,14 +1,28 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { EventBus } from '@nestjs/cqrs';
|
||||
import { mappingMidtransTransaction } from '../../domain/usecases/helpers/mapping-transaction.helper';
|
||||
const midtransClient = require('midtrans-client');
|
||||
import { Snap } from 'midtrans-client';
|
||||
import { MidtransStatus } from '../../domain/entities/midtrans-callback.event';
|
||||
import { TransactionReadService } from 'src/modules/transaction/transaction/data/services/transaction-read.service';
|
||||
|
||||
import * as moment from 'moment';
|
||||
|
||||
@Injectable()
|
||||
export class MidtransService {
|
||||
constructor(private eventBus: EventBus) {}
|
||||
constructor(private transaction: TransactionReadService) {}
|
||||
|
||||
isMoreThan24HoursAgo(dateString) {
|
||||
const date = moment(dateString, 'YYYY-MM-DD', true);
|
||||
if (!date.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = moment();
|
||||
const diffInHours = now.diff(date, 'hours');
|
||||
return diffInHours > 24;
|
||||
}
|
||||
|
||||
get midtransInstance() {
|
||||
return new midtransClient.Snap({
|
||||
return new Snap({
|
||||
isProduction: false,
|
||||
serverKey: process.env.MIDTRANS_SERVER_KEY,
|
||||
clientKey: process.env.MIDTRANS_CLIENT_KEY,
|
||||
|
@ -19,6 +33,32 @@ export class MidtransService {
|
|||
return await this.midtransInstance.transaction.status(orderId);
|
||||
}
|
||||
|
||||
async syncPendingStatus(): Promise<any[]> {
|
||||
const pendingIds = await this.transaction.getPendingOrderId();
|
||||
const responses = [];
|
||||
|
||||
for (const transaction of pendingIds) {
|
||||
const { id, invoice_date } = transaction;
|
||||
let status;
|
||||
try {
|
||||
status = await this.getStatus(id);
|
||||
} catch (error) {
|
||||
status = {
|
||||
order_id: id,
|
||||
transaction_status: this.isMoreThan24HoursAgo(invoice_date)
|
||||
? 'cancel'
|
||||
: 'pending',
|
||||
};
|
||||
}
|
||||
responses.push(status);
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
async changeStatus(orderId: string, action: MidtransStatus): Promise<any> {
|
||||
return await this.midtransInstance.transaction[action](orderId);
|
||||
}
|
||||
|
||||
async create(body): Promise<any> {
|
||||
const data = mappingMidtransTransaction(body);
|
||||
return await this.midtransInstance.createTransaction(data);
|
||||
|
|
|
@ -6,3 +6,10 @@ export interface IEventMidtrans {
|
|||
id: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export enum MidtransStatus {
|
||||
approve = 'approve',
|
||||
deny = 'deny',
|
||||
cancel = 'cancel',
|
||||
expire = 'expire',
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ export function mappingMidtransTransaction(transaction) {
|
|||
const item_details = transaction.items?.map((item) => {
|
||||
return {
|
||||
quantity: Number(item.qty),
|
||||
price: Number(item.total_price),
|
||||
price: Number(item.total_price) / Number(item.qty),
|
||||
name: item.item_name,
|
||||
category: item.item_category_name,
|
||||
};
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
import { Body, Controller, Get, Injectable, Param, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Injectable,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { Public } from 'src/core/guards';
|
||||
import { MidtransService } from '../data/services/midtrans.service';
|
||||
import { EventBus } from '@nestjs/cqrs';
|
||||
import { MidtransCallbackEvent } from '../domain/entities/midtrans-callback.event';
|
||||
import {
|
||||
MidtransCallbackEvent,
|
||||
MidtransStatus,
|
||||
} from '../domain/entities/midtrans-callback.event';
|
||||
import { MidtransDto } from './dto/midtrans.dto';
|
||||
|
||||
@ApiTags(`midtrans`)
|
||||
|
@ -18,7 +30,58 @@ export class MidtransController {
|
|||
|
||||
@Get(':id/status')
|
||||
async getStatus(@Param('id') id: string) {
|
||||
return await this.dataService.getStatus(id);
|
||||
try {
|
||||
const data = await this.dataService.getStatus(id);
|
||||
this.eventBus.publishAll([
|
||||
new MidtransCallbackEvent({
|
||||
id: id,
|
||||
data: data,
|
||||
}),
|
||||
]);
|
||||
|
||||
return 'Berhasil update status transaksi';
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
throw new Error('Gagal update status transaksi');
|
||||
}
|
||||
}
|
||||
|
||||
@Get('sync')
|
||||
async syncStatus() {
|
||||
try {
|
||||
const results = await this.dataService.syncPendingStatus();
|
||||
|
||||
for (const data of results) {
|
||||
this.eventBus.publishAll([
|
||||
new MidtransCallbackEvent({
|
||||
id: data.order_id,
|
||||
data: data,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return 'Berhasil update status transaksi';
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
throw new Error('Gagal update status transaksi');
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/change-status')
|
||||
@ApiQuery({ name: 'status', enum: MidtransStatus })
|
||||
async cancel(
|
||||
@Param('id') id: string,
|
||||
@Query('status') status = MidtransStatus.cancel,
|
||||
) {
|
||||
try {
|
||||
return await this.dataService.changeStatus(id, status);
|
||||
} catch (error) {
|
||||
const data =
|
||||
error.ApiResponse?.status_message ??
|
||||
error.message ??
|
||||
'Gagal update status transaksi';
|
||||
throw new UnprocessableEntityException(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('callback')
|
||||
|
|
|
@ -3,12 +3,20 @@ import { CqrsModule } from '@nestjs/cqrs';
|
|||
import { MidtransController } from './infrastructure/midtrans.controller';
|
||||
import { MidtransService } from './data/services/midtrans.service';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TransactionReadService } from 'src/modules/transaction/transaction/data/services/transaction-read.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';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule.forRoot(), CqrsModule],
|
||||
imports: [
|
||||
ConfigModule.forRoot(),
|
||||
CqrsModule,
|
||||
TypeOrmModule.forFeature([TransactionModel], CONNECTION_NAME.DEFAULT),
|
||||
],
|
||||
controllers: [MidtransController],
|
||||
providers: [MidtransService],
|
||||
providers: [MidtransService, TransactionReadService],
|
||||
exports: [MidtransService],
|
||||
})
|
||||
export class MidtransModule {}
|
||||
|
|
|
@ -24,7 +24,13 @@ export class BatchDeleteItemCategoryManager extends BaseBatchDeleteManager<ItemC
|
|||
}
|
||||
|
||||
get validateRelations(): validateRelations[] {
|
||||
return [{ relation: 'items' }];
|
||||
return [
|
||||
{
|
||||
relation: 'items',
|
||||
message:
|
||||
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get entityTarget(): any {
|
||||
|
|
|
@ -24,7 +24,13 @@ export class BatchInactiveItemCategoryManager extends BaseBatchUpdateStatusManag
|
|||
}
|
||||
|
||||
get validateRelations(): validateRelations[] {
|
||||
return [{ relation: 'items' }];
|
||||
return [
|
||||
{
|
||||
relation: 'items',
|
||||
message:
|
||||
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get entityTarget(): any {
|
||||
|
|
|
@ -27,7 +27,13 @@ export class DeleteItemCategoryManager extends BaseDeleteManager<ItemCategoryEnt
|
|||
}
|
||||
|
||||
get validateRelations(): validateRelations[] {
|
||||
return [{ relation: 'items' }];
|
||||
return [
|
||||
{
|
||||
relation: 'items',
|
||||
message:
|
||||
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get entityTarget(): any {
|
||||
|
|
|
@ -27,7 +27,13 @@ export class InactiveItemCategoryManager extends BaseUpdateStatusManager<ItemCat
|
|||
}
|
||||
|
||||
get validateRelations(): validateRelations[] {
|
||||
return [{ relation: 'items' }];
|
||||
return [
|
||||
{
|
||||
relation: 'items',
|
||||
message:
|
||||
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get entityTarget(): any {
|
||||
|
|
|
@ -27,7 +27,17 @@ export class UpdateItemCategoryManager extends BaseUpdateManager<ItemCategoryEnt
|
|||
}
|
||||
|
||||
get validateRelations(): validateRelations[] {
|
||||
return [];
|
||||
if (this.data.item_type != this.oldData.item_type) {
|
||||
return [
|
||||
{
|
||||
relation: 'items',
|
||||
message:
|
||||
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
get uniqueColumns(): columnUniques[] {
|
||||
|
@ -42,7 +52,6 @@ export class UpdateItemCategoryManager extends BaseUpdateManager<ItemCategoryEnt
|
|||
return [
|
||||
{
|
||||
topic: ItemCategoryUpdatedEvent,
|
||||
data: this.data,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { BaseFilterEntity } from 'src/core/modules/domain/entities/base-filter.entity';
|
||||
import { ItemType } from 'src/modules/item-related/item-category/constants';
|
||||
|
||||
export interface FilterItemRateEntity extends BaseFilterEntity {
|
||||
item_ids: string[];
|
||||
item_types: ItemType[];
|
||||
season_period_ids: string[];
|
||||
start_date: Date;
|
||||
end_date: Date;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue