Compare commits

..

No commits in common. "development" and "master" have entirely different histories.

1138 changed files with 2023 additions and 70355 deletions

View File

@ -1,109 +0,0 @@
kind: pipeline
type: docker
name: server
steps:
# - name: build
# image: appleboy/drone-ssh
# settings:
# 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
# when:
# ref:
# - refs/tags/devel_*
# - refs/tags/*-alpha.*
- name: build-testing
image: plugins/docker
settings:
registry: registry.eigen.co.id
repo: registry.eigen.co.id/eigen/${DRONE_REPO_NAME}
tags: ${DRONE_TAG}
custom_dns: 172.10.10.16
when:
ref:
- refs/tags/*-alpha.*
- name: build-production
image: plugins/docker
settings:
registry: registry.eigen.co.id
repo: registry.eigen.co.id/eigen/${DRONE_REPO_NAME}
tags: ${DRONE_TAG}
custom_dns: 172.10.10.16
when:
ref:
- refs/tags/*-production.*
- 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:
event:
exclude:
- promote
---
kind: pipeline
type: docker
name: kustomize
clone:
disable: true
steps:
- name: kustomize-testing
image: registry.k8s.io/kustomize/kustomize:v5.0.0
environment:
DEVOPS_SSH_PRIVATE:
from_secret: DEVOPS_SSH_PRIVATE
DEVOPS_SSH_PUBLIC:
from_secret: DEVOPS_SSH_PUBLIC
INFRASTRUCTURE_REPO: "k8s-kustomize-external"
DIRECTORY_NAME: "weplay-pos-testing"
commands:
- mkdir -p ~/.ssh &&
- echo $DEVOPS_SSH_PRIVATE | base64 -d > ~/.ssh/id_rsa &&
- echo $DEVOPS_SSH_PUBLIC | base64 -d > ~/.ssh/id_rsa.pub &&
- ssh-keyscan -H -p 2222 git.eigen.co.id >> ~/.ssh/known_hosts &&
- chmod 700 ~/.ssh/ &&
- chmod 600 ~/.ssh/id_rsa &&
- git clone ssh://git@git.eigen.co.id:2222/eigen/$INFRASTRUCTURE_REPO.git &&
- cd $INFRASTRUCTURE_REPO/$DIRECTORY_NAME
- kustomize edit set image registry.eigen.co.id/eigen/$DRONE_REPO_NAME=registry.eigen.co.id/eigen/$DRONE_REPO_NAME:$DRONE_TAG &&
- git add . &&
- |-
git commit -m "feat: update $DRONE_REPO_NAME testing to $DRONE_TAG" &&
- git push origin master
- name: send-message
image: harbor.eigen.co.id/docker.com/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": "ALERT: {{ repo.name }} gagal update dengan tag ${DRONE_TAG}"
}
when:
status:
- failure
trigger:
ref:
include:
- refs/tags/*-alpha.*
depends_on:
- server

View File

@ -1,38 +0,0 @@
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

View File

@ -1,32 +0,0 @@
kind: pipeline
type: docker
name: build
steps:
- name: build-dev
image: plugins/docker
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/09856c08-cf1e-493f-a302-d7cd65b22384
trigger:
ref:
- refs/tags/devel_*
event:
exclude:
- promote
depends_on:
- build

View File

@ -1,27 +0,0 @@
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
- name: deployment
image: alpine
failure: ignore
commands:
- apk add --no-cache curl
- curl -X POST https://manager.sky.eigen.co.id/api/webhooks/09856c08-cf1e-493f-a302-d7cd65b22384
trigger:
ref:
- refs/tags/devel_*
event:
exclude:
- promote
depends_on:
- build

7
.gitignore vendored
View File

@ -30,12 +30,7 @@ lerna-debug.log*
# IDE - VSCode # IDE - VSCode
.vscode/* .vscode/*
.env .env
.dockerignore
docker-compose.yml
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
# IGNORE UPLOAD FOLDER
/uploads

20
.vscode/launch.json vendored
View File

@ -1,20 +0,0 @@
{
// 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

@ -1,17 +0,0 @@
FROM node:18.17-alpine as builder
RUN apk add --no-cache git
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn build
FROM node:18.17-alpine
# ARG env_target
WORKDIR /app
# RUN echo ${env_target}
# COPY env/$env_target /app/.env
# COPY --from=builder /app/env/$env_target .env
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

@ -1,404 +0,0 @@
<!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">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>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

@ -1,412 +0,0 @@
<!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

@ -1,400 +0,0 @@
<!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>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>
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -1,419 +0,0 @@
<!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

@ -1,412 +0,0 @@
<!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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,415 +0,0 @@
<!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

@ -1,409 +0,0 @@
<!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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

View File

@ -1,13 +0,0 @@
{
"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"
}

17
env/.env-development.example vendored Normal file
View File

@ -0,0 +1,17 @@
PORT="3346"
JWT_SECRET="ftyYM4t4kjuj/0ixvIrS18gpdvBJw42NnW71GrFrEhcn0alQkkH7TQIHU5MFFJ1e"
JWT_EXPIRES="24h"
JWT_REFRESH_EXPIRES="7d"
ENC_KEY="921c83f3b90c92dca4ba9b947f99b4c9"
IV="a671a96159e97a4f"
DEFAULT_DB_HOST="localhost"
DEFAULT_DB_PORT="5432"
DEFAULT_DB_USER="postgres"
DEFAULT_DB_PASS="secret"
DEFAULT_DB_NAME="skyworld_pos"
ELASTIC_APM_ACTIVATE=true
ELASTIC_APM_SERVICE_NAME="Skyworld POS"
ELASTIC_APM_SERVER_URL="http://172.10.10.10:8200"

49
env/env.development vendored
View File

@ -1,49 +0,0 @@
PORT="3346"
JWT_SECRET="ftyYM4t4kjuj/0ixvIrS18gpdvBJw42NnW71GrFrEhcn0alQkkH7TQIHU5MFFJ1e"
JWT_EXPIRES="24h"
JWT_REFRESH_EXPIRES="7d"
ENC_KEY="921c83f3b90c92dca4ba9b947f99b4c9"
IV="a671a96159e97a4f"
COUCHDB_CONFIG="http://root:password@172.10.10.2:5970"
DEFAULT_DB_HOST="postgres"
DEFAULT_DB_PORT="5432"
DEFAULT_DB_USER="root"
DEFAULT_DB_PASS="password"
DEFAULT_DB_NAME="pos"
ELASTIC_APM_ACTIVATE=true
ELASTIC_APM_SERVICE_NAME="Skyworld POS"
ELASTIC_APM_SERVER_URL="http://172.10.10.10:8200"
CRON_MIDNIGHT="55 11 * * *"
CRON_EVERY_MINUTE="55 11 * * *"
CRON_EVERY_HOUR="0 * * * *"
EMAIL_HOST=smtp.gmail.com
EMAIL_POST=465
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=SB-Mid-server-kH9_RBZrTwaUkxSrC5vOVaeG
MIDTRANS_CLIENT_KEY=SB-Mid-client-7XLwqG5cgjUmZj-7
EXPORT_LIMIT_PARTITION=200
ASSETS="https://asset.sky.eigen.co.id/"
GOOGLE_CALENDAR_KEY="AIzaSyCSg4P3uC9Z7kD1P4f3rf1BbBaz4Q-M55o"
GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec10bd5@group.calendar.google.com"
SUPERSET_URL=https://dashboard.weplayground.eigen.co.id
SUPERSET_ADMIN_USERNAME=admin
SUPERSET_ADMIN_PASSWORD=admin
WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID=604883366037548
WHATSAPP_BUSINESS_ACCESS_TOKEN=EAAINOvRRiEEBO9yQsYDnYtjHZB7q1nZCwbBpRcxIGMDWajKZBtmWxNRKvPYkS95KQZBsZBOvSFyjiEg5CcCZBZBtaSZApxyV8fiA3cEyVwf7iVZBQP2YCTPRQZArMFeeXbO0uq5TGygmjsIz3M4YxcUHxPzKO4pKxIyxnzcoUZCqCSo1NqQSLVf3a0JyZAwgDXGL55dV

46
env/env.production vendored
View File

@ -1,46 +0,0 @@
PORT="3346"
JWT_SECRET="ftyYM4t4kjuj/0ixvIrS18gpdvBJw42NnW71GrFrEhcn0alQkkH7TQIHU5MFFJ1e"
JWT_EXPIRES="24h"
JWT_REFRESH_EXPIRES="7d"
ENC_KEY="921c83f3b90c92dca4ba9b947f99b4c9"
IV="a671a96159e97a4f"
COUCHDB_CONFIG="http://root:password@172.10.10.2:5970"
DEFAULT_DB_HOST="postgres"
DEFAULT_DB_PORT="5432"
DEFAULT_DB_USER="root"
DEFAULT_DB_PASS="password"
DEFAULT_DB_NAME="pos"
ELASTIC_APM_ACTIVATE=true
ELASTIC_APM_SERVICE_NAME="Skyworld POS"
ELASTIC_APM_SERVER_URL="http://172.10.10.10:8200"
CRON_MIDNIGHT="55 11 * * *"
CRON_EVERY_MINUTE="55 11 * * *"
CRON_EVERY_HOUR="0 * * * *"
EMAIL_HOST=smtp.gmail.com
EMAIL_POST=465
EMAIL_USER=weplayground.app@gmail.com
EMAIL_TOKEN="sonv vwiu khse vtmv"
MIDTRANS_URL=https://app.midtrans.com
MIDTRANS_PRODUCTION=true
MIDTRANS_SERVER_KEY=Mid-server-BZlPCcrWHDuSxW48oxBs5uAl
MIDTRANS_CLIENT_KEY=Mid-client-YhOPuo0NZPNZfiKq
EXPORT_LIMIT_PARTITION=200
ASSETS="https://asset.sky.eigen.co.id/"
GOOGLE_CALENDAR_KEY="AIzaSyCSg4P3uC9Z7kD1P4f3rf1BbBaz4Q-M55o"
GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec10bd5@group.calendar.google.com"
SUPERSET_URL=https://dashboard.weplayground.eigen.co.id
SUPERSET_ADMIN_USERNAME=admin
SUPERSET_ADMIN_PASSWORD=admin
WHATSAPP_BUSINESS_ACCOUNT_NUMBER_ID=604883366037548
WHATSAPP_BUSINESS_ACCESS_TOKEN=EAAINOvRRiEEBO9yQsYDnYtjHZB7q1nZCwbBpRcxIGMDWajKZBtmWxNRKvPYkS95KQZBsZBOvSFyjiEg5CcCZBZBtaSZApxyV8fiA3cEyVwf7iVZBQP2YCTPRQZArMFeeXbO0uq5TGygmjsIz3M4YxcUHxPzKO4pKxIyxnzcoUZCqCSo1NqQSLVf3a0JyZAwgDXGL55dV

View File

@ -1,42 +0,0 @@
## Formula Calculation
### Instalation
```
yarn add mathjs algebra.js
```
### Example
```ts
import * as math from 'mathjs'
import { Equation, parse } from 'algebra.js'
const formula = 'dpp - (dpp*ppn) - (dpp*retribusi) - (dpp*service) - (dpp*ppn3)'
const total = '300000'
const variable = {
ppn: 11,
retribusi: 5000,
service: 5,
ppn3: 5000
}
try {
const x1 = math.simplify(formula, variable).toString()
console.log('Formula ', x1)
const dppFormula = parse(x1)
const totalFormula = parse(total)
const equation = new Equation(totalFormula, dppFormula)
console.log(equation.toString())
const result = equation.solveFor('dpp').toString()
console.log(result)
const value = math.evaluate(result)
console.log(value)
} catch (e) {
console.log(e)
}
```

View File

@ -17,52 +17,26 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json", "test:e2e": "jest --config ./test/jest-e2e.json"
"orm": "ts-node --project ./tsconfig.json -r tsconfig-paths/register ./node_modules/typeorm/cli.js -d ./src/database/ormconfig.ts",
"migration:execute": "yarn run orm migration:run",
"seed:config": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-seeding/dist/cli.js -n ./src/database/seed-ormconfig.ts config",
"seed:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-seeding/dist/cli.js -n ./src/database/seed-ormconfig.ts seed",
"factory:config": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-seeding/dist/cli.js -n ./src/database/seed-data-ormconfig.ts config",
"factory:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-seeding/dist/cli.js -n ./src/database/seed-data-ormconfig.ts seed",
"db:generate": "npm run orm migration:generate"
}, },
"dependencies": { "dependencies": {
"@faker-js/faker": "^8.4.1",
"@nestjs/axios": "^3.0.3",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2", "@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/cqrs": "^10.2.7", "@nestjs/cqrs": "^10.2.7",
"@nestjs/jwt": "^10.2.0", "@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.1.0",
"@nestjs/swagger": "^7.3.1", "@nestjs/swagger": "^7.3.1",
"@nestjs/typeorm": "^10.0.2", "@nestjs/typeorm": "^10.0.2",
"@types/multer": "^1.4.11",
"algebra.js": "^0.2.6",
"axios": "^1.7.5",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"dotenv": "^16.4.5",
"elastic-apm-node": "^4.5.4", "elastic-apm-node": "^4.5.4",
"exceljs": "^4.4.0",
"fs-extra": "^11.2.0",
"googleapis": "^140.0.0",
"gtts": "^0.2.1",
"handlebars": "^4.7.8",
"mathjs": "^13.0.2",
"midtrans-client": "^1.3.1",
"moment": "^2.30.1",
"nano": "^10.1.3", "nano": "^10.1.3",
"nodemailer": "^6.9.14",
"pdfmake": "^0.2.10",
"pg": "^8.11.5", "pg": "^8.11.5",
"plop": "^4.0.1", "plop": "^4.0.1",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.5.0", "rxjs": "^7.5.0",
"typeorm": "^0.3.20", "typeorm": "^0.3.20"
"typeorm-seeding": "^1.6.1"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
@ -70,7 +44,7 @@
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/node": "^20.12.13", "@types/node": "18.11.18",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",
@ -83,7 +57,7 @@
"supertest": "^6.1.3", "supertest": "^6.1.3",
"ts-jest": "29.0.3", "ts-jest": "29.0.3",
"ts-loader": "^9.2.3", "ts-loader": "^9.2.3",
"ts-node": "^10.9.2", "ts-node": "^10.0.0",
"tsconfig-paths": "4.1.1", "tsconfig-paths": "4.1.1",
"typescript": "^4.7.4" "typescript": "^4.7.4"
}, },

View File

@ -1,246 +0,0 @@
const path = require('path');
const fs = require('fs');
module.exports = function (plop) {
plop.setGenerator('module', {
description: 'Create a new module by default',
prompts: [
{
type: 'input',
name: 'name',
message: 'Name: ',
validate: function (value) {
if (/.+/.test(value)) {
return true;
}
return 'Name is required';
},
},
{
type: 'list',
name: 'base',
message: 'Base: ',
choices: function () {
return ['base', 'base status', 'base core'];
},
},
{
type: 'list',
name: 'location',
message: 'Location: ',
choices: function () {
return ['item related', 'user related', 'season related', 'transaction', 'web information'];
},
},
],
actions: function (data) {
if (['base', 'base core'].includes(data.base)) data.orchestrator = 'data'
else if (data.base == 'base status') data.orchestrator = 'data transaction'
const destination = `src/modules/{{dashCase location}}/{{dashCase name}}`;
const result = [
...mappingModule(data.base, destination),
...mappingController(data.base, destination),
...mappingModel(data.base, destination),
...mappingService(destination),
...mappingOrchestrator(data.base, destination),
...mappingManager(data.base, destination)
]
return result
},
})
};
function mappingService(destination) {
const datas = [];
datas.push(
{
type: 'addMany',
destination: `${destination}/data/services`,
templateFiles: `src/core/templates/services/*.hbs`,
base: 'src/core/templates/services',
},
)
return datas;
}
function mappingOrchestrator(base, destination) {
const datas = [];
if (base == 'base status') {
datas.push(
{
type: 'addMany',
destination: `${destination}/domain/usecases`,
templateFiles: `src/core/templates/orchestrators/base-status/*.hbs`,
base: 'src/core/templates/orchestrators/base-status',
},
)
} else {
datas.push(
{
type: 'addMany',
destination: `${destination}/domain/usecases`,
templateFiles: `src/core/templates/orchestrators/base/*.hbs`,
base: 'src/core/templates/orchestrators/base',
},
)
}
datas.push(
{
type: 'addMany',
destination: `${destination}/domain/usecases`,
templateFiles: `src/core/templates/orchestrators/base-read/*.hbs`,
base: 'src/core/templates/orchestrators/base-read',
},
)
return datas;
}
function mappingController(base, destination) {
const datas = [];
if (base == 'base status') {
datas.push(
{
type: 'addMany',
destination: `${destination}/infrastructure`,
templateFiles: `src/core/templates/controllers/base-status/*.hbs`,
base: 'src/core/templates/controllers/base-status',
},
)
} else {
datas.push(
{
type: 'addMany',
destination: `${destination}/infrastructure`,
templateFiles: `src/core/templates/controllers/base/*.hbs`,
base: 'src/core/templates/controllers/base',
},
)
}
datas.push(
{
type: 'addMany',
destination: `${destination}/infrastructure`,
templateFiles: `src/core/templates/controllers/base-read/*.hbs`,
base: 'src/core/templates/controllers/base-read',
},
)
return datas;
}
function mappingModel(base, destination) {
const datas = [];
if (base == 'base status') {
datas.push(
{
type: 'addMany',
destination: `${destination}/domain/entities/event`,
templateFiles: `src/core/templates/events/base-status/*.hbs`,
base: 'src/core/templates/events/base-status',
}
)
}
datas.push(
{
type: 'addMany',
destination: `${destination}/data/models`,
templateFiles: `src/core/templates/models/*.hbs`,
base: 'src/core/templates/models',
},
{
type: 'addMany',
destination: `${destination}/domain/entities`,
templateFiles: `src/core/templates/entities/*.hbs`,
base: 'src/core/templates/entities',
},
{
type: 'addMany',
destination: `${destination}/infrastructure/dto`,
templateFiles: `src/core/templates/dtos/*.hbs`,
base: 'src/core/templates/dtos',
},
{
type: 'addMany',
destination: `${destination}/domain/entities/event`,
templateFiles: `src/core/templates/events/base/*.hbs`,
base: 'src/core/templates/events/base',
}
)
return datas;
}
function mappingModule(base, destination) {
const datas = [];
if (base == 'base status') {
datas.push(
{
type: 'addMany',
destination: destination,
templateFiles: `src/core/templates/modules/base-status/*.hbs`,
base: 'src/core/templates/modules/base-status',
}
)
} else {
datas.push(
{
type: 'addMany',
destination: destination,
templateFiles: `src/core/templates/modules/base/*.hbs`,
base: 'src/core/templates/modules/base',
}
)
}
datas.push(
{
type: 'addMany',
destination: destination,
templateFiles: `src/core/templates/modules/core/*.hbs`,
base: 'src/core/templates/modules/core',
}
)
return datas;
}
function mappingManager(base, destination) {
const datas = [];
const tujuan = `${destination}/domain/usecases/managers`;
if (base == 'base status') {
datas.push(
{
type: 'addMany',
destination: tujuan,
templateFiles: `src/core/templates/managers/manager-statuses/*.hbs`,
base: 'src/core/templates/managers/manager-statuses',
},
)
}
datas.push(
{
type: 'addMany',
destination: tujuan,
templateFiles: `src/core/templates/managers/base/*.hbs`,
base: 'src/core/templates/managers/base',
},
)
return datas;
}

View File

@ -1,5 +1,7 @@
import { Module, Scope } from '@nestjs/common'; import { Module, Scope } from '@nestjs/common';
import { RefreshTokenInterceptor, SessionModule } from './core/sessions'; import { RefreshTokenInterceptor, SessionModule } from './core/sessions';
import { AuthModule } from './auth/auth.module';
import { JWTGuard } from './core/guards';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { HttpExceptionFilter, TransformInterceptor } from './core/response'; import { HttpExceptionFilter, TransformInterceptor } from './core/response';
import { ApmModule } from './core/apm'; import { ApmModule } from './core/apm';
@ -7,96 +9,12 @@ import { CONNECTION_NAME } from './core/strings/constants/base.constants';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { UserPrivilegeModule } from './modules/user-related/user-privilege/user-privilege.module'; import { UserPrivilegeModule } from './modules/user-related/user-privilege/user-privilege.module';
import { UserPrivilegeModel } from './modules/user-related/user-privilege/data/model/user-privilege.model';
import { CqrsModule } from '@nestjs/cqrs'; import { CqrsModule } from '@nestjs/cqrs';
import { CouchModule } from './modules/configuration/couch/couch.module'; import { CouchModule } from './modules/configuration/couch/couch.module';
import { UserPrivilegeModels } from './modules/user-related/user-privilege/constants';
import { RolesGuard } from './core/guards/domain/roles.guard';
import { PrivilegeService } from './core/guards/domain/services/privilege.service';
import { UserModel } from './modules/user-related/user/data/models/user.model';
import { AuthModule } from './modules/configuration/auth/auth.module';
import { UserModule } from './modules/user-related/user/user.module';
import { LogModel } from './modules/configuration/log/data/models/log.model';
import { ErrorLogModel } from './modules/configuration/log/data/models/error-log.model';
import { LogModule } from './modules/configuration/log/log.module';
import { TenantModule } from './modules/user-related/tenant/tenant.module';
import { ItemCategoryModule } from './modules/item-related/item-category/item-category.module';
import { ItemCategoryModel } from './modules/item-related/item-category/data/models/item-category.model';
import { ConstantModule } from './modules/configuration/constant/constant.module';
import { VipCategoryModule } from './modules/transaction/vip-category/vip-category.module';
import { VipCategoryModel } from './modules/transaction/vip-category/data/models/vip-category.model';
import { VipCodeModule } from './modules/transaction/vip-code/vip-code.module';
import { VipCodeModel } from './modules/transaction/vip-code/data/models/vip-code.model';
import { ItemModule } from './modules/item-related/item/item.module';
import { ItemModel } from './modules/item-related/item/data/models/item.model';
import { SeasonTypeModule } from './modules/season-related/season-type/season-type.module';
import { SeasonTypeModel } from './modules/season-related/season-type/data/models/season-type.model';
import { TaxModule } from './modules/transaction/tax/tax.module';
import { TaxModel } from './modules/transaction/tax/data/models/tax.model';
import { SalesPriceFormulaModule } from './modules/transaction/sales-price-formula/sales-price-formula.module';
import { SalesPriceFormulaModel } from './modules/transaction/sales-price-formula/data/models/sales-price-formula.model';
import { ProfitShareFormulaModule } from './modules/transaction/profit-share-formula/profit-share-formula.module';
import { PaymentMethodModule } from './modules/transaction/payment-method/payment-method.module';
import { PaymentMethodModel } from './modules/transaction/payment-method/data/models/payment-method.model';
import { SeasonPeriodModule } from './modules/season-related/season-period/season-period.module';
import { SeasonPeriodModel } from './modules/season-related/season-period/data/models/season-period.model';
import { ItemRateModule } from './modules/item-related/item-rate/item-rate.module';
import { ItemRateModel } from './modules/item-related/item-rate/data/models/item-rate.model';
import { GoogleCalendarModule } from './modules/configuration/google-calendar/google-calendar.module';
import { TransactionModule } from './modules/transaction/transaction/transaction.module';
import { TransactionModel } from './modules/transaction/transaction/data/models/transaction.model';
import {
TransactionBreakdownTaxModel,
TransactionItemBreakdownModel,
TransactionItemModel,
TransactionItemTaxModel,
} from './modules/transaction/transaction/data/models/transaction-item.model';
import { TransactionTaxModel } from './modules/transaction/transaction/data/models/transaction-tax.model';
import { ReconciliationModule } from './modules/transaction/reconciliation/reconciliation.module';
import { ReportModule } from './modules/reports/report/report.module';
import { ReportBookmarkModule } from './modules/reports/report-bookmark/report-bookmark.module';
import { ReportExportModule } from './modules/reports/report-export/report-export.module';
import { ReportBookmarkModel } from './modules/reports/shared/models/report-bookmark.model';
import { ExportReportHistoryModel } from './modules/reports/shared/models/export-report-history.model';
import { CronModule } from './modules/configuration/cron/cron.module';
import { MidtransModule } from './modules/configuration/midtrans/midtrans.module';
import { RefundModule } from './modules/transaction/refund/refund.module';
import { RefundModel } from './modules/transaction/refund/data/models/refund.model';
import { RefundItemModel } from './modules/transaction/refund/data/models/refund-item.model';
import { GateModule } from './modules/web-information/gate/gate.module';
import { GateModel } from './modules/web-information/gate/data/models/gate.model';
import { TermConditionModule } from './modules/web-information/term-condition/term-condition.module';
import { TermConditionModel } from './modules/web-information/term-condition/data/models/term-condition.model';
import { FaqModel } from './modules/web-information/faq/data/models/faq.model';
import { FaqModule } from './modules/web-information/faq/faq.module';
import { UploadModule } from './modules/configuration/upload/upload.module';
import { NewsModule } from './modules/web-information/news/news.module';
import { NewsModel } from './modules/web-information/news/data/models/news.model';
import { BannerModule } from './modules/web-information/banner/banner.module';
import { BannerModel } from './modules/web-information/banner/data/models/banner.model';
import { MailModule } from './modules/configuration/mail/mail.module';
import { PosLogModel } from './modules/configuration/log/data/models/pos-log.model';
import { ExportModule } from './modules/configuration/export/export.module';
import { TransactionDemographyModel } from './modules/transaction/transaction/data/models/transaction-demography.model';
import { SupersetModule } from './modules/configuration/superset/superset.module';
import { GateScanModule } from './modules/gates/gate.module';
import { UserLoginModel } from './modules/user-related/user/data/models/user-login.model';
import { LogUserLoginModel } from './modules/configuration/log/data/models/log-user-login.model';
import { AuthService } from './core/guards/domain/services/auth.service';
import { ReportSummaryModule } from './modules/reports/report-summary/report-summary.module';
import { QueueModule } from './modules/queue/queue.module';
import {
QueueOrderModel,
QueueTicketModel,
QueueItemModel,
QueueModel,
} from './modules/queue/data/models/queue.model';
import { ItemQueueModule } from './modules/item-related/item-queue/item-queue.module';
import { ItemQueueModel } from './modules/item-related/item-queue/data/models/item-queue.model';
import { QueueBucketModel } from './modules/queue/data/models/queue-bucket.model';
@Module({ @Module({
imports: [ imports: [
ApmModule.register(),
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
}), }),
@ -109,116 +27,19 @@ import { QueueBucketModel } from './modules/queue/data/models/queue-bucket.model
password: process.env.DEFAULT_DB_PASS, password: process.env.DEFAULT_DB_PASS,
database: process.env.DEFAULT_DB_NAME, database: process.env.DEFAULT_DB_NAME,
entities: [ entities: [
...UserPrivilegeModels, UserPrivilegeModel,
BannerModel,
ErrorLogModel,
FaqModel,
GateModel,
ItemModel,
ItemCategoryModel,
ItemRateModel,
ItemQueueModel,
LogModel,
LogUserLoginModel,
NewsModel,
PaymentMethodModel,
PosLogModel,
RefundModel,
RefundItemModel,
SalesPriceFormulaModel,
SeasonPeriodModel,
SeasonTypeModel,
TaxModel,
TermConditionModel,
TransactionModel,
TransactionItemModel,
TransactionTaxModel,
TransactionDemographyModel,
TransactionItemBreakdownModel,
TransactionItemTaxModel,
TransactionBreakdownTaxModel,
UserModel,
UserLoginModel,
VipCategoryModel,
VipCodeModel,
// report
ReportBookmarkModel,
ExportReportHistoryModel,
// Queue
QueueOrderModel,
QueueTicketModel,
QueueItemModel,
QueueModel,
QueueBucketModel,
], ],
synchronize: false, synchronize: true,
}), }),
AuthModule,
ConstantModule,
CqrsModule, CqrsModule,
SessionModule,
AuthModule,
CouchModule, CouchModule,
CronModule,
ExportModule,
GoogleCalendarModule,
LogModule,
MailModule,
MidtransModule,
SessionModule,
UploadModule,
// user
TenantModule,
UserModule,
UserPrivilegeModule, UserPrivilegeModule,
// Item
ItemCategoryModule,
ItemModule,
ItemRateModule,
ItemQueueModule,
// transaction
PaymentMethodModule,
ProfitShareFormulaModule,
ReconciliationModule,
RefundModule,
SalesPriceFormulaModule,
TaxModule,
TransactionModule,
VipCategoryModule,
VipCodeModule,
// session
SeasonTypeModule,
SeasonPeriodModule,
// web information
BannerModule,
FaqModule,
GateModule,
NewsModule,
TermConditionModule,
// report
ReportModule,
ReportBookmarkModule,
ReportExportModule,
ReportSummaryModule,
// superset
SupersetModule,
GateScanModule,
QueueModule,
], ],
controllers: [], controllers: [],
providers: [ providers: [
AuthService,
PrivilegeService,
/** /**
* By default all request from client will protect by JWT * By default all request from client will protect by JWT
* if there is some endpoint/function that does'nt require authentication * if there is some endpoint/function that does'nt require authentication
@ -227,7 +48,7 @@ import { QueueBucketModel } from './modules/queue/data/models/queue-bucket.model
{ {
provide: APP_GUARD, provide: APP_GUARD,
scope: Scope.REQUEST, scope: Scope.REQUEST,
useClass: RolesGuard, useClass: JWTGuard,
}, },
{ {
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,

10
src/auth/auth.module.ts Normal file
View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { AuthController } from './controllers/auth.controller';
import { UserDataService } from './data/user.dataservice';
import { AuthService } from './domain/services/auth.service';
@Module({
providers: [AuthService, UserDataService],
controllers: [AuthController],
})
export class AuthModule {}

View File

@ -0,0 +1,32 @@
import { Body, Controller, Get, Post } from '@nestjs/common';
import { Unprotected } from 'src/core/guards';
import { Pagination } from 'src/core/response';
import { PaginationResponse } from 'src/core/response/domain/ok-response.interface';
import { LoginRequest } from '../domain/entities/request.interface';
import { User } from '../domain/entities/user.interface';
import { AuthService } from '../domain/services/auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly service: AuthService) {}
@Unprotected()
@Post()
login(@Body() body: LoginRequest) {
return this.service.createAccessToken(body);
}
@Get()
user() {
return this.service.getUser();
}
@Pagination()
@Get('/all')
async users(): Promise<PaginationResponse<User>> {
return {
data: await this.service.getUsers(),
total: 101,
};
}
}

View File

@ -0,0 +1,54 @@
import { LoginRequest } from '../domain/entities/request.interface';
import { User } from '../domain/entities/user.interface';
const mockUsers: User[] = [
{
id: 1,
name: 'John Doe',
username: 'johndoe',
password: 'password1',
roles: ['admin'],
},
{
id: 2,
name: 'Jane Doe',
username: 'janedoe',
password: 'password2',
roles: ['user'],
},
{
id: 3,
name: 'Jim Brown',
username: 'jimbrown',
password: 'password3',
roles: ['user', 'admin'],
},
{
id: 4,
name: 'Jane Smith',
username: 'janesmith',
password: 'password4',
roles: ['user'],
},
{
id: 5,
name: 'John Smith',
username: 'johnsmith',
password: 'password5',
roles: ['admin'],
},
];
export class UserDataService {
async login({ username, password }: LoginRequest): Promise<User | undefined> {
const user = mockUsers.find((user) => {
return user.username == username && user.password == password;
});
return user;
}
async users(): Promise<User[]> {
return mockUsers;
}
}

View File

@ -0,0 +1,7 @@
export interface User {
id: number;
name: string;
username: string;
password: string;
roles: string[];
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,37 @@
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { SessionService, UserProvider, UsersSession } from 'src/core/sessions';
import { UserDataService } from '../../data/user.dataservice';
import { LoginRequest } from '../entities/request.interface';
import { User } from '../entities/user.interface';
@Injectable()
export class AuthService {
constructor(
private readonly userDataService: UserDataService,
private readonly session: SessionService,
private readonly user: UserProvider,
) {}
async createAccessToken(payload: LoginRequest): Promise<string> {
const user = await this.userDataService.login(payload);
if (!user)
throw new UnprocessableEntityException(`Username or Password not match`);
const token = this.session.createAccessToken({
id: user.id,
// username: user.username,
name: user.name,
// roles: user.roles,
});
return token;
}
getUser(): UsersSession {
return this.user.user;
}
async getUsers(): Promise<User[]> {
return this.userDataService.users();
}
}

View File

@ -1,3 +1 @@
export const UNPROTECTED_URL = 'unprotected_url'; export const UNPROTECTED_URL = 'unprotected_url';
export const PRIVILEGE_KEY = 'privilege_key';
export const MAIN_MENU = 'main_menu';

View File

@ -1,9 +1,7 @@
import { SetMetadata } from '@nestjs/common'; import { SetMetadata } from '@nestjs/common';
import { MAIN_MENU, UNPROTECTED_URL } from '../../constants'; import { UNPROTECTED_URL } from '../../constants';
/** /**
* @deprecated
* Use Public instead
* This decorator will exclude the request from token check * This decorator will exclude the request from token check
* *
* NOTE: * NOTE:
@ -13,9 +11,3 @@ import { MAIN_MENU, UNPROTECTED_URL } from '../../constants';
*/ */
export const Unprotected = (isUnprotected = true) => export const Unprotected = (isUnprotected = true) =>
SetMetadata(UNPROTECTED_URL, isUnprotected); SetMetadata(UNPROTECTED_URL, isUnprotected);
export const Public = (isUnprotected = true) =>
SetMetadata(UNPROTECTED_URL, isUnprotected);
export const MainMenu = () => SetMetadata(MAIN_MENU, true);
export const ExcludePrivilege = () => SetMetadata(MAIN_MENU, true);

View File

@ -2,29 +2,27 @@ import {
Injectable, Injectable,
CanActivate, CanActivate,
ExecutionContext, ExecutionContext,
UnauthorizedException,
Scope, Scope,
Logger, Logger,
UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { SessionService, UsersSession } from 'src/core/sessions'; import { SessionService, UsersSession } from 'src/core/sessions';
import { UNPROTECTED_URL } from '../constants'; import { UNPROTECTED_URL } from '../constants';
import { PrivilegeService } from './services/privilege.service';
import { AuthService } from './services/auth.service';
@Injectable({ scope: Scope.REQUEST }) @Injectable({ scope: Scope.REQUEST })
export class JWTGuard implements CanActivate { export class JWTGuard implements CanActivate {
constructor( constructor(
protected readonly session: SessionService, protected readonly session: SessionService,
protected readonly reflector: Reflector, protected readonly reflector: Reflector,
protected readonly privilege: PrivilegeService,
protected readonly authService: AuthService,
) {} ) {}
protected isPublic = false;
protected userSession: UsersSession; protected userSession: UsersSession;
async canActivate(context: ExecutionContext) { canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
/** /**
* Check if access url is protected or not * Check if access url is protected or not
* By default `isUnprotected` equals `false` * By default `isUnprotected` equals `false`
@ -33,8 +31,6 @@ export class JWTGuard implements CanActivate {
UNPROTECTED_URL, UNPROTECTED_URL,
[context.getHandler(), context.getClass()], [context.getHandler(), context.getClass()],
); );
this.isPublic = isUnprotected;
this.session.setPublic(isUnprotected);
if (isUnprotected) return true; if (isUnprotected) return true;
/** /**
@ -60,29 +56,9 @@ export class JWTGuard implements CanActivate {
*/ */
try { try {
this.userSession = this.session.verifyToken(token); this.userSession = this.session.verifyToken(token);
await this.authService.verifyRegisteredLoginToken(token);
Logger.log(`Access from ${this.userSession.name}`, 'AuthGuard'); Logger.log(`Access from ${this.userSession.name}`, 'AuthGuard');
return true; return true;
} catch (error) { } catch (error) {
const expiredError = error.message;
if (expiredError === 'jwt expired') {
const [, body] = token.split('.');
const bodyToken = JSON.parse(atob(body));
const user = {
role: bodyToken.role,
user_id: bodyToken.id,
username: bodyToken.username,
user_privilege_id: bodyToken.user_privilege_id,
item_id: bodyToken.item_id,
item_name: bodyToken.item_name,
source: bodyToken.source,
};
this.authService.logoutUser(user, token);
}
throw new UnauthorizedException({ throw new UnauthorizedException({
code: 10001, code: 10001,
message: message:

View File

@ -1,36 +1,23 @@
import { import { Injectable, ExecutionContext } from '@nestjs/common';
Injectable, import { Observable } from 'rxjs';
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { JWTGuard } from './jwt.guard'; import { JWTGuard } from './jwt.guard';
import { MAIN_MENU } from '../constants';
@Injectable() @Injectable()
export class RolesGuard extends JWTGuard { export class RolesGuard extends JWTGuard {
async canActivate(context: ExecutionContext): Promise<boolean> { canActivate(
await super.canActivate(context); context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
super.canActivate(context);
// jika endpoint tersebut bukan public, maka lakukan check lanjutan /**
if (!this.isPublic) { * Create function to check if `this.userSession` have access
// Check apakah endpoint ada decorator untuk exlude privilege (@ExcludePrivilege()) * to Read / Create / Update / and Other Action
const excludePrivilege = this.reflector.getAllAndOverride<boolean>( */
MAIN_MENU,
[context.getHandler(), context.getClass()],
);
if (excludePrivilege) return true;
// check apakah dapat akses module
const isNotAllow = await this.privilege.isNotAllowed();
if (isNotAllow) {
throw new ForbiddenException({
statusCode: 10003,
message: `Akses Terlarang, anda tidak punya akses ke module ini!`,
error: 'ACCESS_FORBIDDEN',
});
}
}
/**
* Assign rules to session, So Query can take the rules and give
* the data base on user request
*/
return true; return true;
} }
} }

View File

@ -1,78 +0,0 @@
import {
HttpStatus,
Injectable,
Scope,
UnauthorizedException,
} from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import {
CONNECTION_NAME,
OPERATION,
} from 'src/core/strings/constants/base.constants';
import { DataSource } from 'typeorm';
import { UserRole } from 'src/modules/user-related/user/constants';
import { UserModel } from 'src/modules/user-related/user/data/models/user.model';
import { AppSource, LogUserType } from 'src/core/helpers/constant';
import { EventBus } from '@nestjs/cqrs';
import { LogUserLoginEvent } from 'src/modules/configuration/log/domain/entities/log-user-login.event';
import { UserLoginModel } from 'src/modules/user-related/user/data/models/user-login.model';
interface UserEntity {
user_id: string;
username: string;
role: UserRole;
user_privilege_id: string;
item_id: string;
item_name: string;
source: AppSource;
}
@Injectable({ scope: Scope.REQUEST })
export class AuthService {
constructor(
@InjectDataSource(CONNECTION_NAME.DEFAULT)
protected readonly dataSource: DataSource,
private eventBus: EventBus,
) {}
get repository() {
return this.dataSource.getRepository(UserLoginModel);
}
async logoutUser(user: UserEntity, token: string) {
await this.repository.delete({ login_token: token });
const userLogout = {
type: LogUserType.logout,
created_at: new Date().getTime(),
name: user.username,
user_privilege_id: user.user_privilege_id,
...user,
};
this.eventBus.publish(
new LogUserLoginEvent({
id: user.user_id,
old: null,
data: userLogout,
user: userLogout as any,
description: 'Logout',
module: UserModel.name,
op: OPERATION.UPDATE,
}),
);
}
async verifyRegisteredLoginToken(token: string) {
const data = await this.repository.findOneBy({ login_token: token });
if (!data) {
throw new UnauthorizedException({
statusCode: HttpStatus.UNAUTHORIZED,
message: `Invalid token`,
error: 'Unauthorized',
});
}
}
}

View File

@ -1,74 +0,0 @@
import { ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { InjectDataSource } from '@nestjs/typeorm';
import { getAction } from 'src/core/helpers/path/get-action-from-path.helper';
import { UserProvider } from 'src/core/sessions';
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
import { UserPrivilegeConfigurationModel } from 'src/modules/user-related/user-privilege/data/models/user-privilege-configuration.model';
import { DataSource, IsNull } from 'typeorm';
import { UserRole } from 'src/modules/user-related/user/constants';
import { UserPrivilegeConfigurationEntity } from 'src/modules/user-related/user-privilege/domain/entities/user-privilege-configuration.entity';
@Injectable({ scope: Scope.REQUEST })
export class PrivilegeService {
constructor(
@InjectDataSource(CONNECTION_NAME.DEFAULT)
protected readonly dataSource: DataSource,
@Inject(REQUEST) private readonly request: Request,
protected readonly session: UserProvider,
) {}
get repository() {
return this.dataSource.getRepository(UserPrivilegeConfigurationModel);
}
get user() {
return this.session.user;
}
get action() {
const headerAction = this.request.headers['ex-model-action'] as string;
return headerAction ?? getAction(this.request.method, this.request.path);
}
async isAllowed() {
// jika rolenya adalah superadmin, abaikan dan return true
if (this.user.role == UserRole.SUPERADMIN) return true;
// check privilege dan sesuaikan dengan akse
const configurations = await this.privilegeConfiguration();
return configurations[this.action];
}
async isNotAllowed() {
return !(await this.isAllowed());
}
private moduleKey() {
const headerKey = 'ex-model-key';
const moduleKey = this.request.headers[headerKey] as string;
if (!moduleKey) {
throw new ForbiddenException({
statusCode: 10005,
message: `Akses Terlarang, anda tidak punya akses ke module ini!`,
error: 'MODULE_KEY_NOT_FOUND',
});
}
const [module, menu, sub_menu, section] = moduleKey.split('.');
return { module, menu, sub_menu, section };
}
async privilegeConfiguration(): Promise<UserPrivilegeConfigurationEntity> {
const { module, menu } = this.moduleKey();
return await this.repository.findOne({
select: ['id', 'view', 'create', 'edit', 'delete', 'cancel', 'confirm'],
where: {
user_privilege_id: this.user.user_privilege_id,
module: module,
menu: menu ?? IsNull(),
},
});
}
}

View File

@ -1,11 +0,0 @@
export enum LogUserType {
login = 'login',
logout = 'logout',
}
export enum AppSource {
POS_ADMIN = 'POS_ADMIN',
POS_COUNTER = 'POS_COUNTER',
QUEUE_ADMIN = 'QUEUE_ADMIN',
QUEUE_CUSTOMER = 'QUEUE_CUSTOMER',
}

View File

@ -1,17 +0,0 @@
import { compare, hash } from 'bcrypt';
export async function hashPassword(
password: string,
saltRounds: number,
): Promise<string> {
const hashedPassword = await hash(password, 10);
return hashedPassword;
}
export async function validatePassword(
password: string,
hashedPassword: string,
): Promise<boolean> {
const isPasswordValid = await compare(password, hashedPassword);
return isPasswordValid;
}

View File

@ -1,28 +0,0 @@
import { PrivilegeAction } from 'src/core/strings/constants/privilege.constants';
function containsUuid(str) {
const parts = str.split('/'); // Split the string by "/"
for (const part of parts) {
if (
/^[0-9a-f]{8}\b-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
part,
)
) {
return true;
}
}
return false;
}
export function getAction(method: string, path: string): string {
if (method === 'GET') return PrivilegeAction.VIEW;
else if (method === 'POST') return PrivilegeAction.CREATE;
else if (method === 'DELETE') return PrivilegeAction.DELETE;
else if (method === 'PATCH' || method === 'PUT') {
if (['confirm', 'active', 'inactive'].includes(path))
return PrivilegeAction.CONFIRM;
else if (path.includes('cancel')) return PrivilegeAction.CANCEL;
else return PrivilegeAction.EDIT;
}
return 'forbidden';
}

View File

@ -1,28 +0,0 @@
import * as fs from 'fs-extra';
import * as path from 'path';
export async function MoveFilePathHelper(data) {
const imagePath = data['qr_image'] ?? data['image_url'];
const sourcePath = path.join(__dirname, '../../../../uploads/', imagePath);
const movePath =
'data/' +
imagePath
.split('/')
.filter((item) => !['uploads', 'tmp'].includes(item))
.join('/');
const destinationPath = path.join(
__dirname,
'../../../../uploads/',
movePath,
);
try {
await fs.move(sourcePath, destinationPath);
Object.assign(data, {
image_url: movePath,
});
} catch (error) {
console.log(`Failed! Error move file data`);
}
}

View File

@ -1,46 +0,0 @@
import { extname } from 'path';
import { v4 as uuidv4 } from 'uuid';
import { HttpException, HttpStatus } from '@nestjs/common';
import * as fs from 'fs';
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { diskStorage } from 'multer';
const MB = 1024 * 1024;
const fileFilter = (req, file, callback) => {
if (
file.mimetype.match(/\/(jpg|jpeg|png|flv|mp4|m3u8|ts|3gp|mov|avi|wmv)$/)
) {
callback(null, true);
} else {
callback(
new HttpException(
`Unsupported file type ${extname(file.originalname)}`,
HttpStatus.BAD_REQUEST,
),
false,
);
}
};
const editFileName = (req, file, callback) => {
const fileExtName = extname(file.originalname);
const randomName = uuidv4();
callback(null, `${randomName}${fileExtName}`);
};
const destinationPath = (req, file, cb) => {
let modulePath = req.body.module;
if (req.body.sub_module) modulePath = `${modulePath}/${req.body.sub_module}`;
fs.mkdirSync(`./uploads/tmp/${modulePath}`, { recursive: true });
cb(null, `./uploads/tmp/${modulePath}`);
};
export const StoreFileConfig: MulterOptions = {
storage: diskStorage({
destination: destinationPath,
filename: editFileName,
}),
fileFilter: fileFilter,
};

View File

@ -1,22 +0,0 @@
import { SelectQueryBuilder } from 'typeorm';
export class BetweenQueryHelper {
constructor(
protected baseQuery: SelectQueryBuilder<any>,
protected moduleName: string,
protected columnName: string,
protected from: any,
protected to: any,
protected valueAlias: string,
) {}
getQuery(): SelectQueryBuilder<any> {
return this.baseQuery.andWhere(
`${this.moduleName}.${this.columnName} BETWEEN :from${this.valueAlias} AND :to${this.valueAlias}`,
{
[`from${this.valueAlias}`]: this.from,
[`to${this.valueAlias}`]: this.to,
},
);
}
}

View File

@ -1,54 +0,0 @@
import { HttpStatus, UnprocessableEntityException } from '@nestjs/common';
import { BaseDataService } from 'src/core/modules/data/service/base-data.service';
import { columnUniques } from 'src/core/strings/constants/interface.constants';
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
export class CheckDuplicateHelper {
constructor(
private dataService: BaseDataService<any>,
private tableName: TABLE_NAME,
private duplicateColumn: columnUniques[],
private entity: any,
private entityId?: string,
) {}
async execute() {
for (const columnCheck of this.duplicateColumn) {
const queryBuilder = this.dataService
.getRepository()
.createQueryBuilder(this.tableName);
// process pengecekan column
queryBuilder.where(
`replace(trim(lower(${this.tableName}.${columnCheck.column})), ' ',' ') = :query`,
{
query: this.entity[columnCheck.column]
?.toLowerCase()
.trim()
.replace(/ +(?= )/g, ''),
},
);
// jika ingin check specific data
if (columnCheck.query) {
queryBuilder.andWhere(columnCheck.query);
}
// jika update, akan membawa id. Maka dari itu, jangan validasi diri sendiri
if (this.entityId) {
queryBuilder.andWhere(`id Not In ('${this.entityId}')`);
}
const data_exists = await queryBuilder.getCount();
if (data_exists > 0) {
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: `Gagal! Data dengan ${columnCheck.column} : ${
this.entity[columnCheck.column]
} telah ada`,
error: 'Unprocessable Entity',
});
}
}
}
}

View File

@ -1,87 +0,0 @@
import { Brackets, SelectQueryBuilder } from 'typeorm';
import { WhereInQueryHelper } from './or-where-in-query.helpe';
import { BaseFilterEntity } from 'src/core/modules/domain/entities/base-filter.entity';
import { BetweenQueryHelper } from './between-query.helper';
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
import { ORDER_TYPE, STATUS } from 'src/core/strings/constants/base.constants';
export function setQueryFilterDefault(
queryBuilder: SelectQueryBuilder<any>,
baseFilter: BaseFilterEntity,
tableName: TABLE_NAME,
): SelectQueryBuilder<any> {
// filter berdasarkan id pembuat
if (!!baseFilter.created_ids)
new WhereInQueryHelper(
queryBuilder,
tableName,
'creator_id',
baseFilter.created_ids,
'creator_ids',
).getQuery();
// filter berdasarkan tanggal terakhir dibuat
if (!!baseFilter.created_from && !!baseFilter.created_to)
new BetweenQueryHelper(
queryBuilder,
tableName,
'created_at',
baseFilter.created_from,
baseFilter.created_to,
'created',
).getQuery();
// filter berdasarkan id pengubah
if (!!baseFilter.updated_ids)
new WhereInQueryHelper(
queryBuilder,
tableName,
'editor_id',
baseFilter.updated_ids,
'editor_ids',
).getQuery();
// filter berdasarkan tanggal terakhir update
if (!!baseFilter.updated_from && !!baseFilter.updated_to)
new BetweenQueryHelper(
queryBuilder,
tableName,
'updated_at',
baseFilter.updated_from,
baseFilter.updated_to,
'updated',
).getQuery();
return queryBuilder;
}
export function getOrderBy(
baseFilter: BaseFilterEntity,
queryBuilder: SelectQueryBuilder<any>,
tableName: TABLE_NAME,
) {
let orderBys: string[] = [`${tableName}.created_at`];
const orderType = baseFilter.order_type ?? ORDER_TYPE.DESC;
if (!!baseFilter.order_by) {
orderBys =
baseFilter.order_by.split('.').length > 1
? [`${baseFilter.order_by}`]
: [`${tableName}.${baseFilter.order_by}`];
if (
baseFilter.order_by.split('.').length == 1 &&
baseFilter.order_by.split('.').pop() != 'id'
) {
orderBys.push(`${tableName}.created_at`);
}
}
for (let i = 0; i < orderBys.length; i++) {
if (i == 0) {
queryBuilder.orderBy(orderBys[i], orderType);
} else {
queryBuilder.addOrderBy(orderBys[i], orderType);
}
}
}

View File

@ -1,31 +0,0 @@
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
import { SelectQueryBuilder } from 'typeorm';
export function joinRelationHelper(
relations: string[],
tableName: TABLE_NAME,
queryBuilder: SelectQueryBuilder<any>,
type?: string,
) {
relations.forEach((relation) => {
let alias = relation;
let relationName = `${tableName}.${relation}`;
if (relation.split(' ').length > 1) {
alias = relation.split(' ').pop();
const relationpath = relation.split(' ')[0];
if (relationpath.split('.').length > 1) relationName = relationpath;
else relationName = `${tableName}.${relation.split(' ')[0]}`;
} else if (relation.split('.').length > 1) {
alias = relation.split('.').pop();
relationName = relation;
}
if (type == 'count')
queryBuilder.loadRelationCountAndMap(relationName, relationName, alias);
else if (type == 'select')
queryBuilder.leftJoinAndSelect(relationName, alias);
else queryBuilder.leftJoin(relationName, alias);
});
}

View File

@ -1,20 +0,0 @@
import { SelectQueryBuilder } from 'typeorm';
export class WhereInQueryHelper {
constructor(
protected baseQuery: SelectQueryBuilder<any>,
protected moduleName: string,
protected columnName: string,
protected values: string[],
protected valueAliases: string,
) {}
getQuery(): SelectQueryBuilder<any> {
return this.baseQuery.andWhere(
`${this.moduleName}.${this.columnName} IN (:...${this.valueAliases})`,
{
[this.valueAliases]: this.values,
},
);
}
}

View File

@ -1,20 +0,0 @@
import { SelectQueryBuilder } from 'typeorm';
export class SearchQueryHelper {
constructor(
protected baseQuery: SelectQueryBuilder<any>,
protected moduleName: string,
protected columnName: string,
protected value: string,
protected valueAliases: string,
) {}
getQuery(): SelectQueryBuilder<any> {
return this.baseQuery.andWhere(
`${this.moduleName}.${this.columnName} ILIKE :${this.valueAliases}`,
{
[this.valueAliases]: `%${this.value}%`,
},
);
}
}

View File

@ -1,6 +1,12 @@
import { Param } from 'src/core/modules/domain/entities/base-filter.entity';
import { Brackets, SelectQueryBuilder } from 'typeorm'; import { Brackets, SelectQueryBuilder } from 'typeorm';
export interface Param {
cols: string;
data: string[];
additional?: any[];
leftJoin?: any[];
}
export class SpecificSearchFilter<Entity = any> { export class SpecificSearchFilter<Entity = any> {
constructor( constructor(
private query: SelectQueryBuilder<Entity>, private query: SelectQueryBuilder<Entity>,
@ -20,16 +26,12 @@ export class SpecificSearchFilter<Entity = any> {
new Brackets((qb) => { new Brackets((qb) => {
params.forEach((param) => { params.forEach((param) => {
const { cols, data, additional, leftJoin } = param; const { cols, data, additional, leftJoin } = param;
const columns = cols.split('.');
let arr = data;
if (!columns.includes('status::text')) { const arr = data?.map((el) =>
arr = data?.map((el) => el.includes("'")
el.includes("'") ? `'%${el.trim().replace(/'/g, "''").replace(/\s+/g, ' ')}%'`
? `'%${el.trim().replace(/'/g, "''").replace(/\s+/g, ' ')}%'` : `'%${el.trim().replace(/\s+/g, ' ')}%'`,
: `'%${el.trim().replace(/\s+/g, ' ')}%'`, );
);
}
const aliases = !cols.match(/\./g) const aliases = !cols.match(/\./g)
? this.table.concat(`.${cols}`) ? this.table.concat(`.${cols}`)

View File

@ -1,83 +0,0 @@
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { BaseDataService } from 'src/core/modules/data/service/base-data.service';
import { validateRelations } from 'src/core/strings/constants/interface.constants';
@Injectable()
export class ValidateRelationHelper<Entity> {
constructor(
private dataId: string,
private dataService: BaseDataService<Entity>,
private relations: validateRelations[],
private tableName: string,
) {}
async execute() {
const repository = this.dataService.getRepository();
const queryBuilder = repository.createQueryBuilder(this.tableName);
// load relation
for (const relation of this.relations) {
if (relation.singleQuery) {
queryBuilder.leftJoinAndMapOne(
`${this.tableName}.${relation.relation}`,
`${this.tableName}.${relation.relation}`,
relation.relation,
);
} else if (relation.query) {
queryBuilder.loadRelationCountAndMap(
`${this.tableName}.total_${relation.relation}`,
`${this.tableName}.${relation.relation}`,
`total_${relation.relation}`,
relation.query,
);
} else {
queryBuilder.loadRelationCountAndMap(
`${this.tableName}.total_${relation.relation}`,
`${this.tableName}.${relation.relation}`,
`total_${relation.relation}`,
);
}
}
// filtering data only with specific data
queryBuilder.where(`${this.tableName}.id in ('${this.dataId}')`);
// get data
const data = await queryBuilder.getOne();
// process validasi
for (const relation of this.relations) {
const message =
relation.message ??
`Failed! this data already connected to ${relation.relation}`;
if (relation.singleQuery) {
const relationColumn =
data[relation.relation]?.[`${relation.singleQuery[0]}`];
if (
!!relationColumn &&
this.mappingValidator(
relationColumn,
relation.singleQuery[1],
relation.singleQuery[2],
)
)
throw new UnprocessableEntityException(message);
} else if (data[`total_${relation.relation}`] > 0)
throw new UnprocessableEntityException(message);
}
}
mappingValidator(column, operator, value) {
switch (operator) {
case '!=':
return column != value;
case '==':
return column == value;
default:
return column == value;
}
}
}

View File

@ -1,5 +1,5 @@
import { Entity, PrimaryGeneratedColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn } from "typeorm";
import { BaseCoreEntity } from '../../domain/entities/base-core.entity'; import { BaseCoreEntity } from "../../domain/entities/base-core.entity";
@Entity() @Entity()
export abstract class BaseCoreModel<Entity> implements BaseCoreEntity { export abstract class BaseCoreModel<Entity> implements BaseCoreEntity {

View File

@ -1,13 +1,10 @@
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
import { BaseModel } from './base.model'; import { BaseModel } from "./base.model";
import { STATUS } from 'src/core/strings/constants/base.constants'; import { STATUS } from "src/core/strings/constants/base.constants";
import { BaseStatusEntity } from '../../domain/entities/base-status.entity'; import { BaseStatusEntity } from "../../domain/entities/base-status.entity";
@Entity() @Entity()
export abstract class BaseStatusModel<Entity> export abstract class BaseStatusModel<Entity> extends BaseModel<Entity> implements BaseStatusEntity {
extends BaseModel<Entity>
implements BaseStatusEntity
{
@Column('enum', { name: 'status', enum: STATUS, default: STATUS.DRAFT }) @Column('enum', { name: 'status', enum: STATUS, default: STATUS.DRAFT })
status: STATUS; status: STATUS;
} }

View File

@ -3,10 +3,7 @@ import { BaseCoreModel } from './base-core.model';
import { BaseEntity } from '../../domain/entities/base.entity'; import { BaseEntity } from '../../domain/entities/base.entity';
@Entity() @Entity()
export abstract class BaseModel<Entity> export abstract class BaseModel<Entity> extends BaseCoreModel<Entity> implements BaseEntity {
extends BaseCoreModel<Entity>
implements BaseEntity
{
@Column('varchar', { name: 'creator_id', length: 36, nullable: true }) @Column('varchar', { name: 'creator_id', length: 36, nullable: true })
creator_id: string; creator_id: string;
@ -22,6 +19,6 @@ export abstract class BaseModel<Entity>
@Column({ type: 'bigint', nullable: false }) @Column({ type: 'bigint', nullable: false })
created_at: number; created_at: number;
@Column({ type: 'bigint', nullable: false }) @Column({ type: 'bigint', nullable: false })
updated_at: number; updated_at: number;
} }

View File

@ -1,89 +1,54 @@
import { import { EntityTarget, FindManyOptions, QueryRunner, Repository } from "typeorm";
EntityTarget,
FindManyOptions,
QueryRunner,
Repository,
} from 'typeorm';
export abstract class BaseDataService<Entity> { export abstract class BaseDataService<Entity> {
constructor(private repository: Repository<Entity>) {}
constructor(private repository: Repository<Entity>) {}
getRepository(): Repository<Entity> { getRepository(): Repository<Entity> {
return this.repository; return this.repository;
} }
async create( async create(
queryRunner: QueryRunner, queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>, entityTarget: EntityTarget<Entity>,
entity: Entity, entity: Entity,
): Promise<Entity> { ): Promise<Entity> {
// const newEntity = this.repository.create(entityTarget, entity); const newEntity = queryRunner.manager.create(entityTarget, entity);
return await this.repository.save(entity); return await queryRunner.manager.save(newEntity);
} }
async update(
queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>,
filterUpdate: any,
entity: Entity,
): Promise<Entity> {
const newEntity = await queryRunner.manager.findOne(entityTarget, {
where: filterUpdate,
});
async createMany( if (!newEntity) throw new Error('Data not found!');
queryRunner: QueryRunner, Object.assign(newEntity, entity);
entityTarget: EntityTarget<Entity>, return await queryRunner.manager.save(newEntity);
entity: Entity[], }
): Promise<Entity[]> {
// const newEntity = this.repository.create(entityTarget, entity);
return await this.repository.save(entity);
}
async createBatch( async deleteById(
queryRunner: QueryRunner, queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>, entityTarget: EntityTarget<Entity>,
entity: Entity[], id: string,
): Promise<Entity[]> { ): Promise<void> {
// const newEntity = this.repository.create(entityTarget, entity); await queryRunner.manager.delete(entityTarget, { id });
return await this.repository.save(entity); }
}
async update( async deleteByOptions(
queryRunner: QueryRunner, queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>, entityTarget: EntityTarget<Entity>,
filterUpdate: any, findManyOptions: FindManyOptions<Entity>,
entity: Entity, ): Promise<void> {
): Promise<Entity> { await queryRunner.manager.delete(entityTarget, findManyOptions);
const newEntity = await this.repository.findOne({ }
where: filterUpdate,
});
if (!newEntity) throw new Error('Data not found!'); async getOneByOptions(findOneOptions): Promise<Entity> {
Object.assign(newEntity, entity); return await this.repository.findOne(findOneOptions);
return await this.repository.save(newEntity); }
} }
async deleteById(
queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>,
id: string,
): Promise<void> {
await this.repository.delete(id);
}
async deleteByIds(
queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>,
ids: string[],
): Promise<void> {
await this.repository.delete(ids);
}
async deleteByOptions(
queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>,
findManyOptions: FindManyOptions<Entity>,
): Promise<void> {
const datas = await this.repository.find(findManyOptions);
await this.repository.delete(datas?.map((item) => item['id']));
}
async getOneByOptions(findOneOptions): Promise<Entity> {
return await this.repository.findOne(findOneOptions);
}
async getManyByOptions(findOneOptions): Promise<Entity[]> {
return await this.repository.find(findOneOptions);
}
}

View File

@ -1,42 +1,42 @@
import { FindOneOptions, Repository, SelectQueryBuilder } from 'typeorm'; import { FindOneOptions, Repository, SelectQueryBuilder } from "typeorm";
import { BaseFilterEntity } from '../../domain/entities/base-filter.entity'; import { BaseFilterEntity } from "../../domain/entities/base-filter.entity";
import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; import { PaginationResponse } from "src/core/response/domain/ok-response.interface";
export abstract class BaseReadService<Entity> { export abstract class BaseReadService<Entity> {
constructor(private repository: Repository<Entity>) {}
constructor(private repository: Repository<Entity>) {}
getRepository(): Repository<Entity> { getRepository(): Repository<Entity> {
return this.repository; return this.repository;
} }
async getIndex( async getIndex(
queryBuilder: SelectQueryBuilder<Entity>, queryBuilder: SelectQueryBuilder<Entity>,
params: BaseFilterEntity, params: BaseFilterEntity,
): Promise<PaginationResponse<Entity>> { ): Promise<PaginationResponse<Entity>> {
const limit = params.limit ?? 10; const [data, total] = await queryBuilder
const page = params.page ?? 1; .take(+params.limit)
const [data, total] = await queryBuilder .skip(+params.limit * +params.page - +params.limit)
.take(+limit) .getManyAndCount();
.skip(+limit * +page - +limit)
.getManyAndCount();
return { return {
data, data,
total, total,
}; };
} }
async getOneByOptions(findOneOptions): Promise<Entity> {
return await this.repository.findOne(findOneOptions);
}
async getOneOrFailByOptions(
findOneOptions: FindOneOptions<Entity>,
): Promise<Entity> {
return await this.repository.findOneOrFail(findOneOptions);
}
async getManyByOptions(findManyOptions): Promise<Entity[]> {
return await this.repository.find(findManyOptions);
}
async getOneByOptions(findOneOptions): Promise<Entity> { }
return await this.repository.findOne(findOneOptions);
}
async getOneOrFailByOptions(
findOneOptions: FindOneOptions<Entity>,
): Promise<Entity> {
return await this.repository.findOneOrFail(findOneOptions);
}
async getManyByOptions(findManyOptions): Promise<Entity[]> {
return await this.repository.find(findManyOptions);
}
}

View File

@ -1,41 +1,52 @@
import { Repository, TreeRepository } from 'typeorm'; import { Repository, TreeRepository } from "typeorm";
import { BaseReadService } from './base-read.service'; import { BaseReadService } from "./base-read.service";
export abstract class BaseTreeReadService< export abstract class BaseTreeReadService<Entity> extends BaseReadService<Entity> {
Entity,
> extends BaseReadService<Entity> { constructor(
constructor( private dataRepository: Repository<Entity>,
private dataRepository: Repository<Entity>, private treeRepository: TreeRepository<Entity>
private treeRepository: TreeRepository<Entity>, ) {
) { super(dataRepository);
super(dataRepository); }
}
async findRoots() { async findRoots() {
return this.treeRepository.findRoots(); return this.treeRepository.findRoots()
} }
async findDescendants(parent, relations = []): Promise<Entity[]> { async findDescendants(
return this.treeRepository.findDescendants(parent, { parent,
relations: relations, relations = [],
): Promise<Entity[]> {
return this.treeRepository.findDescendants(parent, {
relations: relations,
});
}
async findDescendantsTree(
parent,
relations = [],
): Promise<Entity> {
return this.treeRepository.findDescendantsTree(parent, {
relations: relations,
});
}
async findAncestors(
parent,
relations = [],
): Promise<Entity[]> {
return await this.treeRepository.findAncestors(parent, {
relations: relations,
}); });
} }
async findDescendantsTree(parent, relations = []): Promise<Entity> { async findAncestorsTree(
return this.treeRepository.findDescendantsTree(parent, { parent,
relations: relations, relations = [],
): Promise<Entity> {
return await this.treeRepository.findAncestorsTree(parent, {
relations: relations,
}); });
} }
}
async findAncestors(parent, relations = []): Promise<Entity[]> {
return await this.treeRepository.findAncestors(parent, {
relations: relations,
});
}
async findAncestorsTree(parent, relations = []): Promise<Entity> {
return await this.treeRepository.findAncestorsTree(parent, {
relations: relations,
});
}
}

View File

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

View File

@ -1,32 +1,18 @@
import { ORDER_TYPE, STATUS } from 'src/core/strings/constants/base.constants'; import { ORDER_TYPE, STATUS } from "src/core/strings/constants/base.constants";
export interface BaseFilterEntity { export interface BaseFilterEntity {
page: number; page: number;
limit: number; limit: number;
q?: string; q?: string;
names: string[]; names: string[];
entity_ids: string[]; entity_ids: string[];
order_by: string; order_by: string;
order_type: ORDER_TYPE; order_type: ORDER_TYPE;
statuses: STATUS[]; statuses: STATUS[];
created_ids: string[]; created_ids: string[];
created_from: number; created_from: number;
created_to: number; created_to: number;
updated_ids: string[]; updated_ids: string[];
updated_from: number; updated_from: number;
updated_to: number; updated_to: number;
} }
export interface Param {
cols: string;
data: string[];
additional?: any[];
leftJoin?: any[];
isStatus?: boolean;
}
export interface RelationParam {
joinRelations: string[];
selectRelations: string[];
countRelations: string[];
}

View File

@ -1,6 +1,6 @@
import { STATUS } from 'src/core/strings/constants/base.constants'; import { STATUS } from "src/core/strings/constants/base.constants";
import { BaseEntity } from './base.entity'; import { BaseEntity } from "./base.entity";
export interface BaseStatusEntity extends BaseEntity { export interface BaseStatusEntity extends BaseEntity {
status: STATUS; status: STATUS;
} }

View File

@ -1,10 +1,10 @@
import { BaseCoreEntity } from './base-core.entity'; import { BaseCoreEntity } from "./base-core.entity";
export interface BaseEntity extends BaseCoreEntity { export interface BaseEntity extends BaseCoreEntity {
creator_id: string; creator_id: string;
creator_name: string; creator_name: string;
editor_id: string; editor_id: string;
editor_name: string; editor_name: string;
created_at: number; created_at: number;
updated_at: number; updated_at: number;
} }

View File

@ -1,57 +1,52 @@
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from "@nestjs/common";
import { UserProvider, UsersSession } from 'src/core/sessions'; import { UserProvider, UsersSession } from "src/core/sessions";
import { BLANK_USER } from 'src/core/strings/constants/base.constants'; import { BLANK_USER } from "src/core/strings/constants/base.constants";
import { TABLE_NAME } from 'src/core/strings/constants/table.constants'; import { TABLE_NAME } from "src/core/strings/constants/table.constants";
import { RelationParam } from '../entities/base-filter.entity';
@Injectable() @Injectable()
export abstract class BaseReadManager { export abstract class BaseReadManager {
public user: UsersSession;
public dataService: any; public user: UsersSession;
public queryBuilder: any; public dataService: any;
protected tableName: TABLE_NAME; public queryBuilder: any;
abstract get relations(): RelationParam; protected tableName: TABLE_NAME;
abstract get selects(): string[]; @Inject()
@Inject() protected userProvider: UserProvider;
protected userProvider: UserProvider;
private readonly baseLog = new Logger(BaseReadManager.name); private readonly baseLog = new Logger(BaseReadManager.name);
setUser() { setUser() {
try { try {
this.user = this.userProvider?.user; this.user = this.userProvider?.user;
} catch (error) { } catch (error) {
this.user = BLANK_USER; this.user = BLANK_USER;
}
} }
}
setService(dataService, tableName) { setService(dataService) {
this.dataService = dataService; this.dataService = dataService;
this.tableName = tableName; this.queryBuilder = this.dataService.getRepository().createQueryBuilder(this.tableName);
this.queryBuilder = this.dataService }
.getRepository()
.createQueryBuilder(this.tableName);
}
async execute(): Promise<void> { async execute(): Promise<void> {
this.baseLog.log(`prepareData`, BaseReadManager.name); this.baseLog.log(`prepareData`, BaseReadManager.name);
await this.prepareData(); await this.prepareData();
this.baseLog.log(`beforeProcess`, BaseReadManager.name); this.baseLog.log(`beforeProcess`, BaseReadManager.name);
await this.beforeProcess(); await this.beforeProcess();
this.baseLog.log('process', BaseReadManager.name);
await this.process();
this.baseLog.log('process', BaseReadManager.name); this.baseLog.log('afterProcess', BaseReadManager.name);
await this.process(); await this.afterProcess();
}
this.baseLog.log('afterProcess', BaseReadManager.name); abstract prepareData(): Promise<void>;
await this.afterProcess();
}
abstract prepareData(): Promise<void>; abstract beforeProcess(): Promise<void>;
abstract beforeProcess(): Promise<void>; abstract process(): Promise<void>;
abstract process(): Promise<void>; abstract afterProcess(): Promise<void>;
}
abstract afterProcess(): Promise<void>;
}

View File

@ -1,102 +1,85 @@
import { Inject, Injectable, Logger } from '@nestjs/common'; import { BadRequestException, Inject, Injectable, Logger } from "@nestjs/common";
import { EventBus } from '@nestjs/cqrs'; import { EventBus } from "@nestjs/cqrs";
import { UserProvider, UsersSession } from 'src/core/sessions'; import { UserProvider, UsersSession } from "src/core/sessions";
import { BLANK_USER } from 'src/core/strings/constants/base.constants'; import { BLANK_USER } from "src/core/strings/constants/base.constants";
import { import { EventTopics } from "src/core/strings/constants/interface.constants";
EventTopics, import { QueryRunner } from "typeorm";
validateRelations,
} from 'src/core/strings/constants/interface.constants';
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
import { QueryRunner } from 'typeorm';
@Injectable() @Injectable()
export abstract class BaseManager { export abstract class BaseManager {
public user: UsersSession; public user: UsersSession;
public dataService: any; public queryRunner: QueryRunner;
protected data: any; public dataService: any;
public queryRunner: QueryRunner; protected data: any;
protected tableName: TABLE_NAME; @Inject()
@Inject() protected userProvider: UserProvider;
protected userProvider: UserProvider; @Inject()
@Inject() protected eventBus: EventBus;
protected eventBus: EventBus;
abstract get validateRelations(): validateRelations[];
// sebagai optional yang dapat digunakan private readonly baseLog = new Logger(BaseManager.name);
public dataServiceFirstOpt: any;
// sebagai optional yang dapat digunakan setUser() {
public dataServiceSecondOpt: any; try {
this.user = this.userProvider?.user;
private readonly baseLog = new Logger(BaseManager.name); } catch (error) {
this.user = BLANK_USER;
setUser() { }
try {
this.user = this.userProvider?.user ?? BLANK_USER;
} catch (error) {
this.user = BLANK_USER;
} }
}
setService( setService(dataService) {
dataService, this.dataService = dataService;
tableName, this.queryRunner = this.dataService.getRepository().manager.connection.createQueryRunner();
dataServiceOpt = null,
dataServiceSecondOpt = null,
) {
this.dataService = dataService;
this.tableName = tableName;
this.queryRunner = this.dataService
.getRepository()
.manager.connection.createQueryRunner(tableName);
if (dataServiceOpt) this.dataServiceFirstOpt = dataServiceOpt;
if (dataServiceSecondOpt) this.dataServiceSecondOpt = dataServiceSecondOpt;
}
abstract get eventTopics(): EventTopics[];
async execute(): Promise<void> {
try {
this.setUser();
this.queryRunner.startTransaction();
this.baseLog.verbose('prepareData');
await this.prepareData();
if (!this.dataService) {
throw new Error('data or service not implemented.');
}
this.baseLog.verbose('validateProcess');
await this.validateProcess();
this.baseLog.verbose('beforeProcess');
await this.beforeProcess();
this.baseLog.verbose('process');
await this.process();
this.baseLog.verbose('afterProcess');
await this.afterProcess();
this.baseLog.verbose('commitTransaction');
await this.queryRunner.commitTransaction();
} catch (e) {
if (e.response) throw new Error(JSON.stringify(e.response));
else throw new Error(e.message);
} finally {
await this.queryRunner.release();
} }
}
abstract prepareData(): Promise<void>; abstract get eventTopics(): EventTopics[];
abstract validateProcess(): Promise<void>; async execute(): Promise<void> {
try {
this.setUser();
abstract beforeProcess(): Promise<void>; this.queryRunner.startTransaction();
this.baseLog.verbose('prepareData');
await this.prepareData();
abstract process(): Promise<void>; if (!this.data || !this.dataService) {
throw new Error("data or service not implemented.");
}
abstract afterProcess(): Promise<void>; this.baseLog.verbose('validateProcess');
} await this.validateProcess();
this.baseLog.verbose('beforeProcess');
await this.beforeProcess();
this.baseLog.verbose('process');
await this.process();
this.baseLog.verbose('afterProcess');
await this.afterProcess();
this.baseLog.verbose('commitTransaction');
await this.queryRunner.commitTransaction();
this.publishEvents();
await this.queryRunner.release();
} catch (e) {
if (e.response) throw new Error(JSON.stringify(e.response));
else throw new Error(e.message);
}
}
abstract prepareData(): Promise<void>;
abstract validateProcess(): Promise<void>;
abstract beforeProcess(): Promise<void>;
abstract process(): Promise<void>;
abstract afterProcess(): Promise<void>;
async publishEvents() {
if (!this.eventTopics.length) return
};
}

View File

@ -1,107 +0,0 @@
import { BatchResult } from 'src/core/response/domain/ok-response.interface';
import { BaseManager } from '../base.manager';
import { HttpStatus, NotFoundException } from '@nestjs/common';
import { ValidateRelationHelper } from 'src/core/helpers/validation/validate-relation.helper';
import { RecordLog } from 'src/modules/configuration/log/domain/entities/log.event';
import { OPERATION } from 'src/core/strings/constants/base.constants';
export abstract class BaseBatchDeleteManager<Entity> extends BaseManager {
protected dataIds: string[];
protected result: BatchResult;
abstract get entityTarget(): any;
setData(ids: string[]): void {
this.dataIds = ids;
}
validateProcess(): Promise<void> {
return;
}
prepareData(): Promise<void> {
return;
}
async process(): Promise<void> {
let totalFailed = 0;
let totalSuccess = 0;
const messages = [];
for (const id of this.dataIds) {
try {
const entity = await this.dataService.getOneByOptions({
where: {
id: id,
},
});
if (!entity) {
throw new NotFoundException({
statusCode: HttpStatus.NOT_FOUND,
message: `Gagal! data dengan id ${id} tidak ditemukan`,
error: 'Entity Not Found',
});
}
await this.validateData(entity);
await new ValidateRelationHelper(
id,
this.dataService,
this.validateRelations,
this.tableName,
).execute();
await this.dataService.deleteById(
this.queryRunner,
this.entityTarget,
id,
);
this.publishEvents(entity, entity);
totalSuccess = totalSuccess + 1;
} catch (error) {
totalFailed = totalFailed + 1;
messages.push(error.response?.message ?? error.message);
}
}
this.result = {
total_items: this.dataIds.length,
total_failed: totalFailed,
total_success: totalSuccess,
messages: messages,
};
}
abstract validateData(data: Entity): Promise<void>;
abstract getResult(): BatchResult;
async publishEvents(dataOld, dataNew) {
this.eventBus.publish(
new RecordLog({
id: dataNew['id'],
old: dataOld,
data: dataNew,
user: this.user,
description: `${this.user.name} delete batch data ${this.tableName}`,
module: this.tableName,
op: OPERATION.DELETE,
}),
);
if (!this.eventTopics.length) return;
for (const topic of this.eventTopics) {
this.eventBus.publishAll([
new topic.topic({
id: dataNew['id'],
old: dataOld,
data: dataNew,
user: this.user,
description: '',
module: this.tableName,
op: OPERATION.UPDATE,
}),
]);
}
}
}

View File

@ -1,173 +0,0 @@
import { BatchResult } from 'src/core/response/domain/ok-response.interface';
import { BaseManager } from '../base.manager';
import { HttpStatus, NotFoundException } from '@nestjs/common';
import { OPERATION, STATUS } from 'src/core/strings/constants/base.constants';
import { ValidateRelationHelper } from 'src/core/helpers/validation/validate-relation.helper';
import { RecordLog } from 'src/modules/configuration/log/domain/entities/log.event';
import * as _ from 'lodash';
export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
protected dataIds: string[];
protected relations: string[] = [];
protected result: BatchResult;
protected dataStatus: STATUS;
protected oldData: Entity;
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;
}
validateProcess(): Promise<void> {
return;
}
prepareData(): Promise<void> {
return;
}
async process(): Promise<void> {
let totalFailed = 0;
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,
},
relations: this.relations,
});
if (!entity) {
throw new NotFoundException({
statusCode: HttpStatus.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 new ValidateRelationHelper(
id,
this.dataService,
this.validateRelations,
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,
{ id: id },
entity,
);
this.publishEvents(this.oldData, result);
totalSuccess = totalSuccess + 1;
} catch (error) {
totalFailed = totalFailed + 1;
messages.push(error.response?.message ?? error.message);
}
}
this.result = {
total_items: this.dataIds.length,
total_failed: totalFailed,
total_success: totalSuccess,
messages: messages,
};
}
abstract validateData(data: Entity): Promise<void>;
abstract getResult(): BatchResult;
async publishEvents(dataOld, dataNew) {
this.eventBus.publish(
new RecordLog({
id: dataNew['id'],
old: dataOld,
data: dataNew,
user: this.user,
description: `${this.user.name} update batch data ${this.tableName}`,
module: this.tableName,
op: OPERATION.UPDATE,
}),
);
if (!this.eventTopics.length) return;
for (const topic of this.eventTopics) {
let data;
if (topic.relations?.length) {
data = await this.dataService.getOneByOptions({
where: {
id: dataNew.id,
},
relations: topic.relations,
});
}
this.eventBus.publishAll([
new topic.topic({
id: data?.['id'] ?? dataNew?.['id'],
old: dataOld,
data: data ?? dataNew,
user: this.user,
description: '',
module: this.tableName,
op: OPERATION.UPDATE,
}),
]);
}
}
}

View File

@ -1,152 +0,0 @@
import { BaseManager } from '../base.manager';
import {
EventTopics,
columnUniques,
validateRelations,
} from 'src/core/strings/constants/interface.constants';
import { HttpStatus, UnprocessableEntityException } from '@nestjs/common';
import { SelectQueryBuilder } from 'typeorm';
export abstract class BaseChangePosition<Entity> extends BaseManager {
protected result: Entity;
protected duplicateColumn: string[];
protected startData: Entity;
protected endData: Entity;
protected columnSort: string;
protected firstDataId: number;
protected lastSort: number;
protected sortTo: number;
abstract get entityTarget(): any;
setData(entity: Entity, columnSort: string): void {
this.data = entity;
this.columnSort = columnSort;
}
async beforeProcess(): Promise<void> {
if (!this.data?.end || this.data.start == this.data?.end) {
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: 'Gagal! tolong pindahkan ke posisi lain',
error: 'Unprocessable Entity',
});
}
this.startData = await this.dataService.getOneByOptions({
where: {
id: this.data.start,
},
});
if (!this.startData) {
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: `Gagal! data dengan id : ${this.data.start} tidak ditemukan`,
error: 'Unprocessable Entity',
});
}
this.endData = await this.dataService.getOneByOptions({
where: {
id: this.data.end,
},
});
if (!this.endData) {
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: `Gagal! data dengan id : ${this.data.end} tidak ditemukan`,
error: 'Unprocessable Entity',
});
}
if (this.endData[this.columnSort] > this.startData[this.columnSort]) {
// drag from up
this.firstDataId = this.startData[this.columnSort];
this.lastSort = this.endData[this.columnSort];
this.sortTo = this.lastSort;
} else if (
this.endData[this.columnSort] < this.startData[this.columnSort]
) {
// drag from bottom
this.firstDataId = this.endData[this.columnSort];
this.lastSort = this.startData[this.columnSort];
this.sortTo = this.firstDataId;
}
}
async prepareData(): Promise<void> {
Object.assign(this.data, {
creator_id: this.user.id,
creator_name: this.user.name,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
});
}
async validateProcess(): Promise<void> {
return;
}
async process(): Promise<void> {
let dataArrange: Entity[];
const queryBuilder = this.dataService
.getRepository()
.createQueryBuilder(this.tableName)
.where(`${this.tableName}.${this.columnSort} between :data1 and :data2`, {
data1: this.firstDataId,
data2: this.lastSort,
});
const datas = await queryBuilder
.orderBy(`${this.tableName}.${this.columnSort}`, 'ASC')
.getManyAndCount();
if (datas[0].length) {
let dataFirst = datas[0][0][this.columnSort];
const data = datas[0];
const length = datas[1];
if (this.endData[this.columnSort] > this.startData[this.columnSort]) {
// drag from above
const dataDragged = data[0];
const arraySlice = data.slice(1, length);
dataArrange = arraySlice.concat([dataDragged]);
} else if (
this.endData[this.columnSort] < this.startData[this.columnSort]
) {
// drag from bottom
const dataDragged = data[length - 1];
const arraySlice = data.slice(0, length - 1);
dataArrange = [dataDragged].concat(arraySlice);
}
for (let i = 0; i < length; i++) {
dataArrange[i][this.columnSort] = dataFirst;
dataFirst++;
}
await this.dataService.createMany(
this.queryRunner,
this.entityTarget,
dataArrange,
);
}
}
get validateRelations(): validateRelations[] {
return [];
}
get eventTopics(): EventTopics[] {
return [];
}
getResult(): string {
return `Success! Data ${this.startData['name']} successfully moved to ${this.sortTo}`;
}
}

View File

@ -1,111 +1,40 @@
import { CheckDuplicateHelper } from 'src/core/helpers/query/check-duplicate.helpers'; import { BaseManager } from "../base.manager";
import { BaseManager } from '../base.manager'; import { Injectable } from "@nestjs/common";
import { RecordLog } from 'src/modules/configuration/log/domain/entities/log.event';
import { OPERATION } from 'src/core/strings/constants/base.constants';
import {
columnUniques,
validateRelations,
} from 'src/core/strings/constants/interface.constants';
import { MoveFilePathHelper } from 'src/core/helpers/path/move-file-path.helper';
@Injectable()
export abstract class BaseCreateManager<Entity> extends BaseManager { export abstract class BaseCreateManager<Entity> extends BaseManager {
protected result: Entity;
protected duplicateColumn: string[]; protected result: Entity;
abstract get entityTarget(): any; protected duplicateColumn: string[];
abstract get uniqueColumns(): columnUniques[]; abstract get entityTarget(): any;
setData(entity: Entity): void { setData(entity: Entity): void {
this.data = entity; this.data = entity;
}
get validateRelations(): validateRelations[] {
return [];
}
async prepareData(): Promise<void> {
Object.assign(this.data, {
creator_id: this.user.id,
creator_name: this.user.name,
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
});
}
async validateProcess(): Promise<void> {
if (this.uniqueColumns.length) {
await new CheckDuplicateHelper(
this.dataService,
this.tableName,
this.uniqueColumns,
this.data,
).execute();
}
return;
}
async process(): Promise<void> {
const keys = Object.keys(this.data);
if (
(keys.includes('qr_image') || keys.includes('image_url')) &&
(this.data['image_url']?.includes('tmp') ||
this.data['qr_image']?.includes('tmp'))
) {
await MoveFilePathHelper(this.data);
} }
this.result = await this.dataService.create( async prepareData(): Promise<void> {
this.queryRunner, Object.assign(this.data, {
this.entityTarget, creator_id: this.user.id,
this.data, creator_name: this.user.name,
); created_at: new Date().getTime(),
updated_at: new Date().getTime(),
this.publishEvents();
}
async getResult(): Promise<Entity> {
return await this.dataService.getOneByOptions({
where: {
id: this.result['id'],
},
});
}
async publishEvents() {
this.eventBus?.publish(
new RecordLog({
id: this.result['id'],
old: null,
data: this.result,
user: this.user,
description: '',
module: this.tableName,
op: OPERATION.CREATE,
}),
);
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: this.result['id'],
old: null,
data: data ?? topic.data,
user: this.user,
description: '',
module: this.tableName,
op: OPERATION.CREATE,
}),
]);
} }
}
} async process(): Promise<void> {
this.result = await this.dataService.create(
this.queryRunner,
this.entityTarget,
this.data,
);
}
async getResult(): Promise<Entity> {
return await this.dataService.getOneByOptions({
where: {
id: this.result['id']
}
})
}
}

View File

@ -1,54 +0,0 @@
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;
abstract get entityTarget(): any;
setData(entity: any): void {
this.data = entity;
}
get validateRelations(): validateRelations[] {
return [];
}
async prepareData(): Promise<void> {
if (this.data)
Object.assign(this.data, {
editor_id: this.user.id,
editor_name: this.user.name,
updated_at: new Date().getTime(),
});
}
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

@ -1,80 +1,41 @@
import { HttpStatus, UnprocessableEntityException } from '@nestjs/common'; import { HttpStatus, Injectable, UnauthorizedException, UnprocessableEntityException } from "@nestjs/common";
import { BaseManager } from '../base.manager'; import { BaseManager } from "../base.manager";
import { ValidateRelationHelper } from 'src/core/helpers/validation/validate-relation.helper';
import { RecordLog } from 'src/modules/configuration/log/domain/entities/log.event';
import { OPERATION } from 'src/core/strings/constants/base.constants';
@Injectable()
export abstract class BaseDeleteManager<Entity> extends BaseManager { export abstract class BaseDeleteManager<Entity> extends BaseManager {
protected dataId: string;
protected result: Entity; protected dataId: string;
abstract get entityTarget(): any; protected result: Entity;
abstract get entityTarget(): any;
setData(id: string): void { setData(id: string): void {
this.dataId = id; this.dataId = id;
}
async prepareData(): Promise<void> {
this.data = await this.dataService.getOneByOptions({
where: {
id: this.dataId,
},
});
if (!this.data)
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: `Gagal! Data denga id ${this.dataId} tidak ditemukan`,
error: 'Unprocessable Entity',
});
return;
}
async process(): Promise<void> {
await new ValidateRelationHelper(
this.dataId,
this.dataService,
this.validateRelations,
this.tableName,
).execute();
await this.dataService.deleteById(
this.queryRunner,
this.entityTarget,
this.dataId,
);
this.publishEvents();
}
abstract getResult(): string;
async publishEvents() {
this.eventBus.publish(
new RecordLog({
id: this.data['id'],
old: null,
data: this.data,
user: this.user,
description: `${this.user.name} delete data ${this.tableName}`,
module: this.tableName,
op: OPERATION.CREATE,
}),
);
if (!this.eventTopics.length) return;
for (const topic of this.eventTopics) {
this.eventBus.publishAll([
new topic.topic({
id: topic.data['id'],
old: this.data,
data: topic.data,
user: this.user,
description: '',
module: this.tableName,
op: OPERATION.DELETE,
}),
]);
} }
}
} async prepareData(): Promise<void> {
this.data = await this.dataService.getOneByOptions({
where: {
id: this.dataId
}
})
if (!this.data)
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: `Data with id ${this.dataId} not found`,
error: 'Unprocessable Entity',
});
return;
}
async process(): Promise<void> {
await this.dataService.deleteById(
this.queryRunner,
this.entityTarget,
this.dataId,
);
}
abstract getResult(): string;
}

View File

@ -1,40 +1,24 @@
import { joinRelationHelper } from 'src/core/helpers/query/join-relations.helper'; import { BaseReadManager } from "../base-read.manager";
import { BaseReadManager } from '../base-read.manager';
export abstract class BaseDetailManager<Entity> extends BaseReadManager { export abstract class BaseDetailManager<Entity> extends BaseReadManager {
protected dataId: string;
protected result: Entity;
abstract get setFindProperties(): any;
setData(dataId: string): void { protected dataId: string;
this.dataId = dataId; protected result: Entity;
}
async process(): Promise<void> { abstract get selectData(): string[];
const { joinRelations, selectRelations, countRelations } = this.relations; abstract get relationData(): string[];
if (joinRelations?.length) abstract get setFindProperties(): any;
joinRelationHelper(joinRelations, this.tableName, this.queryBuilder);
if (selectRelations?.length)
joinRelationHelper(
selectRelations,
this.tableName,
this.queryBuilder,
'select',
);
if (countRelations?.length)
joinRelationHelper(
countRelations,
this.tableName,
this.queryBuilder,
'count',
);
if (this.selects?.length) this.queryBuilder.select(this.selects); setData(dataId: string): void {
this.queryBuilder.where(this.setFindProperties); this.dataId = dataId;
this.result = await this.queryBuilder.getOne(); }
}
getResult(): Entity { async process(): Promise<void> {
return this.result; this.queryBuilder.select(this.selectData).where(this.setFindProperties);
} this.result = await this.queryBuilder.getOne();
} }
getResult(): Entity {
return this.result;
}
}

View File

@ -1,88 +1,54 @@
import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; import { PaginationResponse } from "src/core/response/domain/ok-response.interface";
import { BaseReadManager } from '../base-read.manager'; import { BaseReadManager } from "../base-read.manager";
import { SelectQueryBuilder } from 'typeorm'; import { SelectQueryBuilder } from "typeorm";
import { SpecificSearchFilter } from 'src/core/helpers/query/specific-search.helper'; import { BaseFilterEntity } from "../../entities/base-filter.entity";
import { import { Param, SpecificSearchFilter } from "src/core/helpers/query/specific-search.helper";
getOrderBy,
setQueryFilterDefault,
} from 'src/core/helpers/query/default-filter.helper';
import { Param } from '../../entities/base-filter.entity';
import { joinRelationHelper } from 'src/core/helpers/query/join-relations.helper';
import { STATUS } from 'src/core/strings/constants/base.constants';
export abstract class BaseIndexManager<Entity> extends BaseReadManager { export abstract class BaseIndexManager<Entity> extends BaseReadManager {
protected result: PaginationResponse<Entity>;
public filterParam: any; protected result: PaginationResponse<Entity>;
abstract get specificFilter(): Param[]; public filterParam: BaseFilterEntity;
abstract get specificFilter(): Param[];
setFilterParam(param: any): void { setFilterParam(param: BaseFilterEntity): void {
this.filterParam = param; this.filterParam = param;
}
async process(): Promise<void> {
const specificFilter = this.specificFilter;
const { joinRelations, selectRelations, countRelations } = this.relations;
if (joinRelations?.length)
joinRelationHelper(joinRelations, this.tableName, this.queryBuilder);
if (selectRelations?.length)
joinRelationHelper(
selectRelations,
this.tableName,
this.queryBuilder,
'select',
);
if (countRelations?.length)
joinRelationHelper(
countRelations,
this.tableName,
this.queryBuilder,
'count',
);
if (this.selects?.length) this.queryBuilder.select(this.selects);
if (this.filterParam.statuses?.length > 0) {
const data = this.filterParam.statuses.map((status) => {
const statusData = status.includes("'")
? status.trim().replace(/'/g, "''").replace(/\s+/g, ' ')
: status.trim().replace(/\s+/g, ' ');
// 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()]}'`;
});
const exist = specificFilter.find((item) => item.isStatus);
if (!exist) {
specificFilter.push({
cols: `${this.tableName}.status::text`,
data: data,
});
}
} }
new SpecificSearchFilter<Entity>( async process(): Promise<void> {
this.queryBuilder, // const filterSearch: string[] = this.setFilterSearch();
this.tableName,
specificFilter,
).getFilter();
getOrderBy(this.filterParam, this.queryBuilder, this.tableName); // this.queryBuilder.andWhere(
this.setQueryFilter(this.queryBuilder); // new Brackets((qb) => {
setQueryFilterDefault(this.queryBuilder, this.filterParam, this.tableName); // filterSearch.map((fSearch) => {
// qb.orWhere(`${fSearch} ILIKE :query`, {
// query: `%${
// this.filterParam.q.trim().replace(/\s+/g, ' ') ?? ''
// }%`,
// });
// });
// }),
// );
this.result = await this.dataService.getIndex( new SpecificSearchFilter<Entity>(this.queryBuilder, this.tableName, this.specificFilter).getFilter();
this.queryBuilder, this.setQueryFilter(this.queryBuilder);
this.filterParam,
);
}
abstract setQueryFilter( this.result = await this.dataService.getIndex(
queryBuilder: SelectQueryBuilder<Entity>, this.queryBuilder,
): SelectQueryBuilder<Entity>; this.filterParam,
);
}
getResult(): PaginationResponse<Entity> { setFilterSearch(): string[] {
return this.result; return [];
} }
}
abstract setQueryFilter(
queryBuilder: SelectQueryBuilder<Entity>,
): SelectQueryBuilder<Entity>;
getResult(): PaginationResponse<Entity> {
return this.result;
}
}

View File

@ -1,126 +1,42 @@
import { ValidateRelationHelper } from 'src/core/helpers/validation/validate-relation.helper'; import { Injectable } from "@nestjs/common";
import { BaseManager } from '../base.manager'; import { BaseManager } from "../base.manager";
import { import { STATUS } from "src/core/strings/constants/base.constants";
OPERATION, import { UserPrivilegeModel } from "src/modules/user-related/user-privilege/data/model/user-privilege.model";
QUEUE_STATUS,
STATUS,
} from 'src/core/strings/constants/base.constants';
import * as _ from 'lodash';
import { RecordLog } from 'src/modules/configuration/log/domain/entities/log.event';
@Injectable()
export abstract class BaseUpdateStatusManager<Entity> extends BaseManager { export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
protected dataId: string;
protected result: Entity;
protected oldData: Entity;
protected dataStatus: STATUS | QUEUE_STATUS;
protected relations = [];
protected duplicateColumn: string[];
abstract get entityTarget(): any;
setData(id: string, status: STATUS | QUEUE_STATUS): void { protected dataId: string;
/** protected result: Entity;
* // TODO: Handle case confirm multiple tabs; protected dataStatus: STATUS;
* Pola id yang dikirim dirubah menjadi data_id___updated_at protected duplicateColumn: string[];
* Untuk mendapatkan value id nya saja dan menghindari breaking change abstract get entityTarget(): any;
* 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; setData(id: string, status: STATUS): void {
this.dataStatus = status; this.dataId = id;
} this.dataStatus = status;
async prepareData(): Promise<void> {
this.data = await this.dataService.getOneByOptions({
where: {
id: this.dataId,
},
relations: this.relations,
});
this.oldData = _.cloneDeep(this.data);
Object.assign(this.data, {
editor_id: this.user.id,
editor_name: this.user.name,
updated_at: new Date().getTime(),
status: this.dataStatus,
});
}
async process(): Promise<void> {
await new ValidateRelationHelper(
this.dataId,
this.dataService,
this.validateRelations,
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,
{ id: this.dataId },
this.data,
);
this.publishEvents();
}
abstract getResult(): string;
async publishEvents() {
this.eventBus.publish(
new RecordLog({
id: this.result['id'],
old: this.oldData,
data: this.result,
user: this.user,
description: `${this.user.name} update status data ${this.tableName} to ${this.dataStatus}`,
module: this.tableName,
op: OPERATION.UPDATE,
}),
);
if (!this.eventTopics.length) return;
for (const topic of this.eventTopics) {
let data;
if (!topic.data) {
data = await this.dataService.getOneByOptions({
where: {
id: this.dataId,
},
relations: topic.relations,
});
}
this.eventBus.publishAll([
new topic.topic({
id: data?.['id'] ?? topic?.data?.['id'],
old: this.oldData,
data: data ?? topic.data,
user: this.user,
description: '',
module: this.tableName,
op: OPERATION.UPDATE,
}),
]);
} }
}
} async prepareData(): Promise<void> {
this.data = new UserPrivilegeModel();
Object.assign(this.data, {
editor_id: this.user.id,
editor_name: this.user.name,
updated_at: new Date().getTime(),
id: this.dataId,
status: this.dataStatus,
});
}
async process(): Promise<void> {
this.result = await this.dataService.update(
this.queryRunner,
this.entityTarget,
{ id: this.dataId },
this.data,
);
}
abstract getResult(): string;
}

View File

@ -1,116 +1,41 @@
import { CheckDuplicateHelper } from 'src/core/helpers/query/check-duplicate.helpers'; import { Injectable } from "@nestjs/common";
import { BaseManager } from '../base.manager'; import { BaseManager } from "../base.manager";
import { ValidateRelationHelper } from 'src/core/helpers/validation/validate-relation.helper';
import { columnUniques } from 'src/core/strings/constants/interface.constants';
import { RecordLog } from 'src/modules/configuration/log/domain/entities/log.event';
import { OPERATION } from 'src/core/strings/constants/base.constants';
import { HttpStatus, NotFoundException } from '@nestjs/common';
@Injectable()
export abstract class BaseUpdateManager<Entity> extends BaseManager { export abstract class BaseUpdateManager<Entity> extends BaseManager {
protected dataId: string;
protected result: Entity;
protected oldData: Entity;
protected duplicateColumn: string[];
abstract get entityTarget(): any;
abstract get uniqueColumns(): columnUniques[];
setData(id: string, entity: Entity): void { protected dataId: string;
this.dataId = id; protected result: Entity;
this.data = entity; protected duplicateColumn: string[];
} abstract get entityTarget(): any;
async prepareData(): Promise<void> { setData(id: string, entity: Entity): void {
this.oldData = await this.dataService.getOneByOptions({ this.dataId = id;
where: { id: this.dataId }, this.data = entity;
});
if (!this.oldData) {
throw new NotFoundException({
statusCode: HttpStatus.NOT_FOUND,
message: `Gagal! Data denga id ${this.dataId} tidak ditemukan`,
error: 'Entity Not Found',
});
} }
Object.assign(this.data, { async prepareData(): Promise<void> {
editor_id: this.user.id, Object.assign(this.data, {
editor_name: this.user.name, editor_id: this.user.id,
updated_at: new Date().getTime(), editor_name: this.user.name,
}); updated_at: new Date().getTime(),
if (this.uniqueColumns.length) {
await new CheckDuplicateHelper(
this.dataService,
this.tableName,
this.uniqueColumns,
this.data,
this.dataId,
).execute();
}
}
async process(): Promise<void> {
await new ValidateRelationHelper(
this.dataId,
this.dataService,
this.validateRelations,
this.tableName,
).execute();
this.result = await this.dataService.update(
this.queryRunner,
this.entityTarget,
{ id: this.dataId },
this.data,
);
this.publishEvents();
}
async getResult(): Promise<Entity> {
return await this.dataService.getOneByOptions({
where: {
id: this.dataId,
},
});
}
async publishEvents() {
this.eventBus.publish(
new RecordLog({
id: this.result['id'],
old: this.oldData,
data: this.result,
user: this.user,
description: `${this.user.name} update data ${this.tableName}`,
module: this.tableName,
op: OPERATION.UPDATE,
}),
);
if (!this.eventTopics.length) return;
for (const topic of this.eventTopics) {
let data;
if (!topic.data) {
data = await this.dataService.getOneByOptions({
where: {
id: this.dataId,
},
relations: topic.relations,
}); });
}
this.eventBus.publishAll([
new topic.topic({
id: topic.data?.['id'] ?? this.dataId,
old: this.oldData,
data: data ?? topic.data,
user: this.user,
description: '',
module: this.tableName,
op: OPERATION.UPDATE,
}),
]);
} }
}
} async process(): Promise<void> {
this.result = await this.dataService.update(
this.queryRunner,
this.entityTarget,
{ id: this.dataId },
this.data,
);
}
async getResult(): Promise<Entity> {
return await this.dataService.getOneByOptions({
where: {
id: this.dataId
}
})
}
}

View File

@ -1,13 +1,7 @@
import { BatchResult } from 'src/core/response/domain/ok-response.interface'; import { BaseDataOrchestrator } from "./base-data.orchestrator";
import { BaseDataOrchestrator } from './base-data.orchestrator';
export abstract class BaseDataTransactionOrchestrator< export abstract class BaseDataTransactionOrchestrator<Entity> extends BaseDataOrchestrator<Entity> {
Entity, abstract active(dataId: string): Promise<String>;
> extends BaseDataOrchestrator<Entity> { abstract confirm(dataId: string): Promise<String>;
abstract active(dataId: string): Promise<string>; abstract inactive(dataId: string): Promise<String>;
abstract confirm(dataId: string): Promise<string>; }
abstract inactive(dataId: string): Promise<string>;
abstract batchConfirm(dataIds: string[]): Promise<BatchResult>;
abstract batchActive(dataIds: string[]): Promise<BatchResult>;
abstract batchInactive(dataIds: string[]): Promise<BatchResult>;
}

View File

@ -1,8 +1,7 @@
import { BatchResult } from 'src/core/response/domain/ok-response.interface';
export abstract class BaseDataOrchestrator<Entity> { export abstract class BaseDataOrchestrator<Entity> {
abstract create(data: Entity): Promise<Entity>;
abstract update(dataId: string, data: Entity): Promise<Entity>; abstract create(data: Entity): Promise<Entity>;
abstract delete(dataId: string): Promise<string>; abstract update(dataId: string, data: Entity): Promise<Entity>;
abstract batchDelete(dataIds: string[]): Promise<BatchResult>; abstract delete(dataId: string): Promise<String>;
}
}

View File

@ -1,6 +1,6 @@
import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; import { PaginationResponse } from "src/core/response/domain/ok-response.interface";
export abstract class BaseReadOrchestrator<Entity> { export abstract class BaseReadOrchestrator<Entity> {
abstract index(params): Promise<PaginationResponse<Entity>>; abstract index(params): Promise<PaginationResponse<Entity>>;
abstract detail(dataId: string): Promise<Entity>; abstract detail(dataId: string): Promise<Entity>;
} }

View File

@ -1,6 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
export class BatchIdsDto {
@ApiProperty({ type: [String] })
ids: string[];
}

View File

@ -1,4 +0,0 @@
export class ChangePositionDto {
start: string;
end: string;
}

View File

@ -1,5 +1,5 @@
import { BaseCoreEntity } from '../../domain/entities/base-core.entity'; import { BaseCoreEntity } from "../../domain/entities/base-core.entity";
export class BaseCoreDto implements BaseCoreEntity { export class BaseCoreDto implements BaseCoreEntity {
id: string; id: string;
} }

View File

@ -1,14 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from "@nestjs/swagger";
import { BaseFilterEntity } from '../../domain/entities/base-filter.entity'; import { BaseFilterEntity } from "../../domain/entities/base-filter.entity";
import { Transform } from 'class-transformer'; import { Transform } from "class-transformer";
import { import { IsArray, IsEnum, IsNumber, IsString, ValidateIf } from "class-validator";
IsArray, import { ORDER_TYPE, STATUS } from "src/core/strings/constants/base.constants";
IsEnum,
IsNumber,
IsString,
ValidateIf,
} from 'class-validator';
import { ORDER_TYPE, STATUS } from 'src/core/strings/constants/base.constants';
export class BaseFilterDto implements BaseFilterEntity { export class BaseFilterDto implements BaseFilterEntity {
@ApiProperty({ type: Number, required: false, default: 1 }) @ApiProperty({ type: Number, required: false, default: 1 })
@ -22,7 +16,7 @@ export class BaseFilterDto implements BaseFilterEntity {
@ValidateIf((body) => body.limit) @ValidateIf((body) => body.limit)
@IsNumber() @IsNumber()
limit = 10; limit = 10;
@ApiProperty({ type: String, required: false }) @ApiProperty({ type: String, required: false })
q: string; q: string;
@ -61,6 +55,12 @@ export class BaseFilterDto implements BaseFilterEntity {
}) })
@IsArray() @IsArray()
@IsString({ each: true }) @IsString({ each: true })
@IsEnum(STATUS, {
message: `Status must be a valid enum ${JSON.stringify(
Object.values(STATUS),
)}`,
each: true,
})
statuses: STATUS[]; statuses: STATUS[];
@ApiProperty({ type: [String], required: false }) @ApiProperty({ type: [String], required: false })

Some files were not shown because too many files have changed in this diff Show More