Merge branch 'development' into staging

staging
shancheas 2024-08-14 12:51:27 +07:00
commit afa63667ab
231 changed files with 7590 additions and 856 deletions

View File

@ -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

38
.drone.yml.old Normal file
View File

@ -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

20
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

View File

@ -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"]

View File

@ -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>&nbsp;</td>

View File

@ -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>&nbsp;</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>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -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>&nbsp;</td>

View File

@ -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>&nbsp;</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>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -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>&nbsp;</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>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -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>&nbsp;</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>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -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>&nbsp;</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>&nbsp;</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.

BIN
assets/image/logo.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -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"
}

18
env/env.development vendored
View File

@ -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"

15
env/env.production vendored
View File

@ -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"

View File

@ -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",

View File

@ -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,

View File

@ -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',
});
}

View File

@ -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',
});
}

View File

@ -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',
});
}

View File

@ -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}`)

View File

@ -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> {

View File

@ -1,3 +1,3 @@
export interface BaseCoreEntity {
id: string;
id?: string;
}

View File

@ -22,6 +22,7 @@ export interface Param {
data: string[];
additional?: any[];
leftJoin?: any[];
isStatus?: boolean;
}
export interface RelationParam {

View File

@ -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();
}
}

View File

@ -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',
});
}

View File

@ -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,

View File

@ -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',
});
}

View File

@ -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,

View File

@ -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,
}),
]);
}
}
}

View File

@ -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',
});

View File

@ -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>(

View File

@ -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,

View File

@ -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,

View File

@ -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',
}

View File

@ -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,
},
];

View File

@ -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',

View File

@ -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"`);
}
}

View File

@ -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"`,
);
}
}

View File

@ -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"`,
);
}
}

View File

@ -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"`,
);
}
}

View File

@ -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"`,
);
}
}

View File

@ -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`,
);
}
}

View File

@ -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`);
}
}

View File

@ -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"`,
);
}
}

View File

@ -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"`,
);
}
}

View File

@ -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`,
);
}
}

View File

@ -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',
});
}

View File

@ -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);
}
}

View File

@ -1 +1,6 @@
export const DatabaseListen = ['transaction', 'vip_code'];
export const DatabaseListen = [
'transaction',
'vip_code',
'pos_activity',
'pos_cash_activity',
];

View File

@ -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,

View File

@ -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;
}
}
}

View File

@ -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,

View File

@ -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',
);
}
}
}

View File

@ -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',
);
}
}
}
}

View File

@ -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',
);
}
}
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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!",
];
}
}

View File

@ -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,
},
};
}

View File

@ -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 {}

View File

@ -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();
// }
}

View File

@ -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,
},
};
}

View File

@ -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';

View File

@ -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 {}

View File

@ -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';

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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',
}

View File

@ -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);
}
}

View File

@ -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');
}
}
}

View File

@ -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,
],
})

View File

@ -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);
}
}
}

View File

@ -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';
}
}

View File

@ -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');
}
}

View File

@ -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,

View File

@ -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);

View File

@ -6,3 +6,10 @@ export interface IEventMidtrans {
id: string;
data: any;
}
export enum MidtransStatus {
approve = 'approve',
deny = 'deny',
cancel = 'cancel',
expire = 'expire',
}

View File

@ -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,
};

View File

@ -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')

View File

@ -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 {}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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,
},
];
}

View File

@ -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