Compare commits
754 Commits
master
...
developmen
Author | SHA1 | Date |
---|---|---|
|
064112e731 | |
|
d6a238a224 | |
|
dc5e938f75 | |
|
eb4da7ccc4 | |
|
46caaba6bd | |
|
7953c7dbbd | |
|
6911f6f0a2 | |
|
762340a72b | |
|
61cbbf81ef | |
|
4ea53f7088 | |
|
0548141e5f | |
|
29e4dc5400 | |
|
f9d8f85179 | |
|
769e8174f4 | |
|
3ace59c450 | |
|
187555a543 | |
|
551bd12f5b | |
|
1692c8234a | |
|
903b7cfd18 | |
|
26a6af2044 | |
|
d7ed77934d | |
|
66720c5b8b | |
|
44e74de315 | |
|
82e7879969 | |
|
d612b6725a | |
|
54b9658075 | |
|
661dbb8cf2 | |
|
6dc6579450 | |
|
e3db958e0d | |
|
d73752252a | |
|
b38c489777 | |
|
8cfc003261 | |
|
55e89426d9 | |
|
5f6214eeb6 | |
|
d71c2096b8 | |
|
f661276c58 | |
|
4a3e77043e | |
|
b4f141e628 | |
|
9db5c4b326 | |
|
f7e3d5399c | |
|
7305dfd068 | |
|
b4bc31463e | |
|
83d53847d7 | |
|
e09190df42 | |
|
61045a39ab | |
|
60b5bcf638 | |
|
4eedca12e7 | |
|
cb452cf5f3 | |
|
ea25e0cae1 | |
|
5e4401a974 | |
|
c05af5c16b | |
|
5988a592ac | |
|
1148a72481 | |
|
3b7d7ea80b | |
|
3b19484f29 | |
|
4dc21c4ebd | |
|
99d0fc8560 | |
|
ac3fde14e3 | |
|
8d54c686fc | |
|
c0a68db9f1 | |
|
3cf4fbdada | |
|
9eacb7dca5 | |
|
eae529bce9 | |
|
a0de16575f | |
|
53ef4656eb | |
|
ec916ab574 | |
|
72827aa83e | |
|
0e9ae569ba | |
|
37334ecb19 | |
|
86c73058fd | |
|
fb7f925c78 | |
|
2b132c53a8 | |
|
e9535749d4 | |
|
94696e765d | |
|
9805b9903a | |
|
af9818d44c | |
|
2bcd7a34fb | |
|
577f8ea9ea | |
|
1878d03c0f | |
|
6807d00cbe | |
|
ad3e3593fd | |
|
8e7e43b09d | |
|
6c53610ec4 | |
|
c129a59d47 | |
|
d413f1fa7b | |
|
bf5914af92 | |
|
28c1ab36da | |
|
3ed25e1366 | |
|
4c403293db | |
|
dac42b754c | |
|
5ef7521e9b | |
|
ba7b81c320 | |
|
7137b98043 | |
|
3ddd837622 | |
|
217362193a | |
|
a3647b536b | |
|
5f46432327 | |
|
319d9eecef | |
|
50e7f66bb7 | |
|
492a4ca2ba | |
|
4ed7ecce5e | |
|
e6787aed89 | |
|
1d54b709b6 | |
|
bd32d4fbdd | |
|
78fb5b7fac | |
|
dea9989914 | |
|
c8d0b32cc6 | |
|
93b1208278 | |
|
86251f43a2 | |
|
9977a7456a | |
|
487f59ae93 | |
|
e9de46fff8 | |
|
5b507a1c3c | |
|
143bf76417 | |
|
ac86289182 | |
|
32d4064f0a | |
|
01b2796c26 | |
|
e9d864c922 | |
|
d6c02ac29f | |
|
4f0b378ec6 | |
|
efa245048a | |
|
2d0ccf67f2 | |
|
77cf19c06e | |
|
77a6afbbbc | |
|
fea0420ae4 | |
|
3eee8d73f4 | |
|
07d2ec3b46 | |
|
8ba5646bb6 | |
|
ce1aa86944 | |
|
4d20955764 | |
|
6a8816aa90 | |
|
8f43907091 | |
|
7afe06e96c | |
|
c0a0b2316d | |
|
95ee8dce8d | |
|
55ddc9b605 | |
|
e2a6878e71 | |
|
f2c409fc35 | |
|
7213e7915b | |
|
0e65fb8a9a | |
|
6efe5618cb | |
|
60167cd807 | |
|
0dfaeb2045 | |
|
18dc15e442 | |
|
198dcb4933 | |
|
901c67137b | |
|
411458fe4c | |
|
d911f80ff9 | |
|
79f3966b49 | |
|
e6066b534c | |
|
c5590ab7b1 | |
|
1a633cc574 | |
|
1348f5a79b | |
|
90ab3668b4 | |
|
49b98cd56c | |
|
d6ae891de4 | |
|
9026f85a66 | |
|
d40261e919 | |
|
e1c6b809e4 | |
|
c47f1e2616 | |
|
df0536157a | |
|
92bf8fc342 | |
|
437793a2a1 | |
|
48a48e6e7a | |
|
22f4e732db | |
|
34ae5964c4 | |
|
f2fef65f20 | |
|
039531de3b | |
|
0b8bb72392 | |
|
256c8f38bd | |
|
109898b076 | |
|
b2be2e0160 | |
|
b4266d5d68 | |
|
f23a9f3510 | |
|
91370940d7 | |
|
c15b4d8079 | |
|
b16edb73e3 | |
|
ea58096287 | |
|
1dfcaf7b15 | |
|
8b58598955 | |
|
5eb50c952f | |
|
aaa0ca6f76 | |
|
3de7bfbbe5 | |
|
60c03fefa0 | |
|
b34d54e7d0 | |
|
3fd97b8879 | |
|
93c822f34e | |
|
e09c76309e | |
|
43deb04d92 | |
|
23043fb7f9 | |
|
1d377b574c | |
|
7d9f619858 | |
|
c06a2a0a2b | |
|
cc71814648 | |
|
004dfc9de5 | |
|
09d6dbaab2 | |
|
c76594d767 | |
|
f4cf5178b8 | |
|
ffbbf6e140 | |
|
614a9346fb | |
|
f7c49d27d5 | |
|
2ee96a617f | |
|
10049abc55 | |
|
01fbedab77 | |
|
b2659def9a | |
|
d597734467 | |
|
27a0c56af7 | |
|
50ac2d97a4 | |
|
d788a9f1a1 | |
|
c897e4fcde | |
|
14dd2880bc | |
|
3e920755bd | |
|
88b4c66139 | |
|
52c82a9a41 | |
|
19494b3328 | |
|
e1f2cdfa4d | |
|
918055beb9 | |
|
283b783007 | |
|
a2be2bb331 | |
|
d708ef9eee | |
|
9709c4719b | |
|
d2db62339f | |
|
07d1cc78e2 | |
|
8f430574ed | |
|
f7198010d3 | |
|
a1641504f1 | |
|
6c5019f814 | |
|
ff0ef28783 | |
|
40aafaf571 | |
|
6fbb2e33b2 | |
|
b20385dacc | |
|
6e5b19380b | |
|
ab9ec4ac0e | |
|
0457fc9e1d | |
|
3658ae4cdf | |
|
d1b20e6b96 | |
|
f4ecbf0e66 | |
|
7da22277f1 | |
|
bf73cb6b43 | |
|
6fb582204a | |
|
d283caa898 | |
|
1320492bf1 | |
|
4c25b2cbec | |
|
a6b1e5f49f | |
|
ac3ee266b9 | |
|
3720df31d8 | |
|
7fb2995f38 | |
|
07e7b86cd4 | |
|
db57b1973e | |
|
92d60d4496 | |
|
b716c75a2c | |
|
45306dde57 | |
|
539676aa30 | |
|
1a2a37d185 | |
|
5666c31dfa | |
|
665eacd39f | |
|
9d98003a2d | |
|
45c4bde838 | |
|
b9927da0c4 | |
|
d523009acd | |
|
457ce30cc1 | |
|
e7a7ffb2bc | |
|
2527437577 | |
|
ce372de1fd | |
|
7e7d40ea1a | |
|
8590165755 | |
|
cb9421622d | |
|
dc97d5e14a | |
|
79adf156db | |
|
9f50d56cea | |
|
4a13730231 | |
|
c3950c7041 | |
|
464c10722c | |
|
9815c667f0 | |
|
84b829a7fa | |
|
eaf0f43a24 | |
|
22b418b257 | |
|
d5adc48d9b | |
|
009576c841 | |
|
7c7b121b49 | |
|
8abdbb7b55 | |
|
fc15cb9db6 | |
|
9a72c40984 | |
|
f2bc4dd46d | |
|
f4387767a8 | |
|
8b82f9b7db | |
|
ee232447b6 | |
|
a2a9c16619 | |
|
6227555671 | |
|
fa286820fc | |
|
e966de6158 | |
|
50e8951b71 | |
|
cefeef8854 | |
|
f30a23d3c4 | |
|
dcf0a55dfc | |
|
3678353f34 | |
|
ab903d4554 | |
|
bd13f50bc0 | |
|
f4939ffe89 | |
|
4fe385bca1 | |
|
be7c74ec72 | |
|
88cc9f4bc3 | |
|
dc595dfb07 | |
|
211a1e8a3c | |
|
08e018e16b | |
|
cc62910493 | |
|
ff44be66e2 | |
|
b9b1695dc5 | |
|
617a08e3f2 | |
|
aaf6f97e57 | |
|
538bd0e58e | |
|
ceae06779e | |
|
5e78669b6c | |
|
a015353990 | |
|
01b4ee2bbd | |
|
c65f0dc9dc | |
|
c98b8da9ba | |
|
bca3682826 | |
|
11aeb44bac | |
|
07347c9244 | |
|
29cc6dfae6 | |
|
f9937d84ab | |
|
a14119719c | |
|
ae75578b65 | |
|
93fa32df80 | |
|
f7d7fba267 | |
|
ebcc5515e5 | |
|
b597a2f184 | |
|
7a6c784612 | |
|
d6fc817cef | |
|
89aa2a68b4 | |
|
64d812bef1 | |
|
3b8310581b | |
|
21b9549b52 | |
|
76b518614f | |
|
841f8889ec | |
|
bc8476a56e | |
|
fe6572a770 | |
|
acf8861823 | |
|
a319b64abe | |
|
3938504fd3 | |
|
3b07c8de99 | |
|
fc67c222f8 | |
|
f0c1410532 | |
|
c5fdca615f | |
|
e00a6aae31 | |
|
f5425ccfb1 | |
|
995f4b963c | |
|
13895394b3 | |
|
b36e35ea00 | |
|
e9c819987e | |
|
a61f8b853d | |
|
efe5661a57 | |
|
30d3d91bba | |
|
1fa4d315be | |
|
bcdf656d5d | |
|
63152ec90e | |
|
990d73bdb1 | |
|
1dae9ec356 | |
|
8de744bc58 | |
|
f8618ec0cd | |
|
1e8a07ec55 | |
|
810a1c5bf5 | |
|
1d8a3d7ff9 | |
|
b57da1d21a | |
|
8dd36042eb | |
|
72e47c2486 | |
|
cfb0d3c60f | |
|
0c3abc6a8f | |
|
1fcbe57018 | |
|
ba6ee4a408 | |
|
e4a631b929 | |
|
dbdc7a0203 | |
|
fcaf4a07a1 | |
|
9efa56b2bc | |
|
0658b503bd | |
|
9802d983bb | |
|
583b754315 | |
|
4ef139b2f5 | |
|
b137e1abce | |
|
e8baaa12d2 | |
|
6ca74ad6f7 | |
|
955f00cc9d | |
|
b48a469720 | |
|
61d8f56385 | |
|
2a42d85814 | |
|
bacdb1773b | |
|
655157239d | |
|
33014394d1 | |
|
aaee458df6 | |
|
ffcbf65d9d | |
|
2f19db8dd7 | |
|
e7664be8a8 | |
|
a2c1af2a65 | |
|
26db7d2745 | |
|
e922db827d | |
|
5dde995ab8 | |
|
79d1c564ff | |
|
e7abc7db13 | |
|
81a463e761 | |
|
c741a55577 | |
|
81dad9a69c | |
|
8999ed1bf0 | |
|
ebfcbb85fe | |
|
feee9a3439 | |
|
b05e6fab5a | |
|
37350e454f | |
|
b08325a53c | |
|
a1f7108bc5 | |
|
2e4d5df17a | |
|
4fcd852d8d | |
|
0f05656ca2 | |
|
0bec3c6590 | |
|
8eee99fce5 | |
|
239e2d778a | |
|
cc01b23e2a | |
|
0b502188af | |
|
e38fbc65bd | |
|
0e47b99ca7 | |
|
e401a1bf4c | |
|
1636f6b930 | |
|
d1892e7aa5 | |
|
d14d9101ae | |
|
746e24feb6 | |
|
39d7eb28b9 | |
|
9d0d76120e | |
|
971407612c | |
|
03ec533e86 | |
|
d413bd771b | |
|
11171b2859 | |
|
1e9cc9da4f | |
|
46307774e1 | |
|
62eccf29a5 | |
|
383fdce9f7 | |
|
1ae7f4e097 | |
|
33f955c209 | |
|
d4bd3d746c | |
|
45f11003f2 | |
|
2188d63943 | |
|
3af56fa5d5 | |
|
77d8e7ae1e | |
|
c49bb379bd | |
|
0648eeffee | |
|
e8deba2882 | |
|
19386c336e | |
|
58cf3f7ab0 | |
|
d7c4b27749 | |
|
0ac5754170 | |
|
b7557a5f19 | |
|
1590080468 | |
|
61c512f7f4 | |
|
30ffdca550 | |
|
c154af6cb7 | |
|
197b3478ae | |
|
c6be42299e | |
|
9bcc72a69e | |
|
dc6476a66f | |
|
4a9ca5eb5b | |
|
e1004b3843 | |
|
6632222c4b | |
|
945edbf76d | |
|
190f42d598 | |
|
d9bbe1290d | |
|
3e85d40885 | |
|
7e38a67e80 | |
|
b91d9f7da8 | |
|
bf8987242b | |
|
57f2700c94 | |
|
b3f0752ca0 | |
|
33438f37ec | |
|
593955574a | |
|
4ac711add5 | |
|
c7fa402663 | |
|
6096771597 | |
|
76fa380f1e | |
|
1f4bf80908 | |
|
66c481c9b0 | |
|
9cd50ad817 | |
|
82105e8214 | |
|
e68524380a | |
|
d333ba03a7 | |
|
a63eaf0110 | |
|
e32a7c2eaf | |
|
07f379a6b0 | |
|
39896fed90 | |
|
a671404947 | |
|
00d5eba7ab | |
|
d0163ae003 | |
|
aa550f9d38 | |
|
2fc80c7cfc | |
|
20848aab3c | |
|
d81eaac4f6 | |
|
679b98d198 | |
|
1ff5461311 | |
|
2f9c96bdec | |
|
b1bc05fcb9 | |
|
ef2697a3b1 | |
|
000c3f1800 | |
|
c5cec31ab5 | |
|
cc92ef26a1 | |
|
1560cb0512 | |
|
fba7b9aae5 | |
|
0b236cb879 | |
|
41f4773df6 | |
|
908834cdd9 | |
|
4840abf18e | |
|
cd6a5737e7 | |
|
853543ece5 | |
|
37b12c960f | |
|
8c9d0d6585 | |
|
2551521539 | |
|
403a75c835 | |
|
12359f3685 | |
|
d20fd4e175 | |
|
05c98dcb14 | |
|
d5bafe4f4f | |
|
6ca3766457 | |
|
9ac1fa9ae2 | |
|
4178b72af9 | |
|
bfa8d6d524 | |
|
7a74711834 | |
|
9056b60937 | |
|
75663c4f99 | |
|
c2b5f27b3b | |
|
84f7ed6d09 | |
|
eaa1e64899 | |
|
bad83aef69 | |
|
fc36997b91 | |
|
7e18580540 | |
|
e64799f7de | |
|
78ae2a5d8e | |
|
b574d3a39d | |
|
eafd815463 | |
|
5a96282bce | |
|
55d25644dd | |
|
7a3d4b9432 | |
|
af1a642907 | |
|
0d2e49f93a | |
|
688be5828d | |
|
001f371244 | |
|
2a8d5e87cf | |
|
5c0886316c | |
|
daf325b1a0 | |
|
2ed4ce0199 | |
|
66fd9c16b2 | |
|
9242f43760 | |
|
de2041ae95 | |
|
eb32584205 | |
|
f4969a837c | |
|
59cf933abc | |
|
2f03f18d51 | |
|
12543ea00b | |
|
c9b55c3d91 | |
|
0d7951d5f0 | |
|
34909a30b0 | |
|
99847ce85b | |
|
28dcac39f0 | |
|
6261592339 | |
|
c87c852509 | |
|
99261f37cd | |
|
56e7d25acd | |
|
66a5245e61 | |
|
d149a15530 | |
|
0df0f30de5 | |
|
bc25b9d020 | |
|
df1b130dc6 | |
|
3f33ab6850 | |
|
fa2bbbf5e2 | |
|
be738bb49b | |
|
edf8ab173d | |
|
7e50ef1bf8 | |
|
677732d511 | |
|
f023e01c6c | |
|
32ae6481e6 | |
|
389c26e1b0 | |
|
2038f0a74f | |
|
1827cdb592 | |
|
a2e05b4d59 | |
|
94bb218d50 | |
|
d48ac84244 | |
|
91a01a2857 | |
|
313843591e | |
|
36b48b9257 | |
|
b6d067c9cd | |
|
40c92dacd1 | |
|
1cd933d64f | |
|
4aa03226de | |
|
0db0f75cb9 | |
|
e99071f9ce | |
|
8cdf75c2a0 | |
|
d2fd665236 | |
|
c69bdc2295 | |
|
3e83ee3077 | |
|
ce6343dfa2 | |
|
5f3e05a9e3 | |
|
cafbb82af6 | |
|
b9c5cb17d3 | |
|
f9c36582e1 | |
|
ed66b88dd0 | |
|
6fbccb0c9d | |
|
d4d4101d69 | |
|
5ab14f5c62 | |
|
0410b481e1 | |
|
fc37e0c502 | |
|
85d461c70a | |
|
4dc9f7ee99 | |
|
c3ffb2b13f | |
|
6bb8c928c0 | |
|
603632b6af | |
|
dba03ff81c | |
|
1d8cc6e13d | |
|
fec27cf294 | |
|
e2135c841f | |
|
241501815b | |
|
e586a7bad1 | |
|
c0f8d5ad9d | |
|
e139c2bcd8 | |
|
35b3cfac8c | |
|
5dfe9da0ff | |
|
0a7678471d | |
|
7bd66b47a4 | |
|
ac522bc55a | |
|
36d430484e | |
|
db005426bd | |
|
a84a48c292 | |
|
f62dc15075 | |
|
e51e5a51a1 | |
|
8db2256852 | |
|
0b1cdabea4 | |
|
4b31dd40b5 | |
|
3bb6ed1082 | |
|
ced08bebd3 | |
|
8ce58981c7 | |
|
1e0766dc50 | |
|
9f471aafbf | |
|
03769873d0 | |
|
67d0b4b262 | |
|
c862bf2289 | |
|
655229ddbd | |
|
bc296cc52a | |
|
0bed18b439 | |
|
c35f966b5a | |
|
cb8b1bfd6b | |
|
0368f3452b | |
|
cc7a345fff | |
|
067025312e | |
|
5fa89e8666 | |
|
bc0d8cbb10 | |
|
9445d561ac | |
|
23f5ed0946 | |
|
507bc99510 | |
|
9315854143 | |
|
84bf55011d | |
|
766909798f | |
|
f170c2e017 | |
|
a3d67ff5be | |
|
86f81f3027 | |
|
54c5738676 | |
|
5b5c3efccc | |
|
08dea7965d | |
|
99a82c164a | |
|
f159b905ec | |
|
bc8b79b9c9 | |
|
5880a6051f | |
|
554cda2144 | |
|
af6266c8d6 | |
|
84e65d2599 | |
|
aa3c8fa359 | |
|
1ebf6a5c30 | |
|
30b8aa2083 | |
|
0ce8c8402e | |
|
d9d22779f0 | |
|
f7d5a5ca5f | |
|
d98f16b1bf | |
|
f9e9818af9 | |
|
abe163e80a | |
|
d7937217ab | |
|
73547451d4 | |
|
446295ee16 | |
|
50cbaa38c1 | |
|
321d13568f | |
|
afbf22cc03 | |
|
3f3da30c37 | |
|
b2b0ade6b4 | |
|
f5c4b1ffdf | |
|
a126dd267c | |
|
f86f86fe0c | |
|
f4cc5ae1c9 | |
|
d98246063d | |
|
be7eb23c97 | |
|
fd5cb0062d | |
|
74ccb047b1 | |
|
cf46b2fd4c | |
|
f4ddbe08a8 | |
|
a8322e13e5 | |
|
f35aa7412f | |
|
33f269b56b | |
|
63b9a3028a | |
|
14559290cf | |
|
417e90dbc5 | |
|
cfb896921b | |
|
9431e27013 | |
|
ac31c0c2f9 | |
|
d5f0fe6517 | |
|
caa9ea14fc | |
|
91dc7ad5b4 | |
|
e9d2ba952c | |
|
71c3f91a52 | |
|
9ff1012813 | |
|
26a073968f | |
|
c5c5762823 | |
|
5802dc3c92 | |
|
1abb30649a | |
|
4fc428e3b7 | |
|
7bd79bffd2 | |
|
63a849ed7b | |
|
1a3ecefa0a | |
|
47aa9bef55 | |
|
eb90b43429 | |
|
6165763a0b | |
|
ca45d78e1d | |
|
f9ec18ff28 | |
|
df6adf0e0f | |
|
1aae9d0a1b | |
|
f36e3b26d4 | |
|
ede43c1b91 | |
|
cefff6d5c7 | |
|
6596c776e8 | |
|
d3dce3990d | |
|
d83028e148 | |
|
c719102b60 | |
|
b4f868f738 | |
|
1ac5aca57d | |
|
1641c81a53 | |
|
e5da3d1cba | |
|
8b7d1fcd24 | |
|
1ef5404853 | |
|
2fccb2b8e9 | |
|
c7d09a00b5 | |
|
f20fc252c7 | |
|
8fc61a697f | |
|
41330cc74a | |
|
da9145078c | |
|
35b9ffd306 | |
|
f5c1008ece | |
|
34ea6a501d | |
|
98f9fecc27 | |
|
e442ca717e | |
|
af06f2d08e | |
|
240bbf4c2e | |
|
c0b0ffa8fa | |
|
b945ef1e10 | |
|
9ce4c58dc1 | |
|
46b8d50078 |
|
@ -0,0 +1,109 @@
|
|||
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
|
|
@ -0,0 +1,38 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: build
|
||||
steps:
|
||||
- name: build-dev
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: registry.eigen.co.id
|
||||
repo: registry.eigen.co.id/eigen/${DRONE_REPO_NAME}
|
||||
build_args:
|
||||
- env_target=env.development
|
||||
tags: latest
|
||||
custom_dns: 172.10.10.16
|
||||
trigger:
|
||||
ref:
|
||||
- refs/tags/devel_*
|
||||
event:
|
||||
exclude:
|
||||
- promote
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: deployment
|
||||
steps:
|
||||
- name: deployment
|
||||
image: alpine
|
||||
failure: ignore
|
||||
commands:
|
||||
- apk add --no-cache curl
|
||||
- curl -X POST https://manager.sky.eigen.co.id/api/webhooks/806de7e2-1d3e-4889-b472-a59af0a5eb33
|
||||
trigger:
|
||||
ref:
|
||||
- refs/tags/devel_*
|
||||
event:
|
||||
exclude:
|
||||
- promote
|
||||
depends_on:
|
||||
- build
|
|
@ -0,0 +1,32 @@
|
|||
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
|
|
@ -0,0 +1,27 @@
|
|||
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
|
|
@ -30,7 +30,12 @@ lerna-debug.log*
|
|||
# IDE - VSCode
|
||||
.vscode/*
|
||||
.env
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# IGNORE UPLOAD FOLDER
|
||||
/uploads
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Nest Framework",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
|
||||
"autoAttachChildProcesses": true,
|
||||
"restart": true,
|
||||
"sourceMaps": true,
|
||||
"stopOnEntry": false,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
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"]
|
|
@ -0,0 +1,404 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
|
||||
down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
Margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
Margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
Margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
Margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
Margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn>tbody>tr>td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
<table class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p class="mb0">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> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,412 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
|
||||
down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
Margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
Margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
Margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
Margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
Margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn>tbody>tr>td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
<table class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p>Thank you fot choosing us! We're absolutelty thrilled and can't wait to embark on this exciting day with you. See you soon for fun times ahead</p>
|
||||
|
||||
<p class="mb0">Here's a quick recap of your invoice:</p>
|
||||
<p class="mb0">Booking Date: {{booking_date}}</p>
|
||||
<p>Total Invoice: {{payment_total}}</p>
|
||||
|
||||
<p class="mb0">Just a friendly reminder that your invoice will expire on {{expire_date}}</p>
|
||||
<p>To keep things running smoothly, please ensure your payment is completed before this data</p>
|
||||
|
||||
<p>
|
||||
For your convenience, here is a list of our account details:
|
||||
<ul>
|
||||
{{#each payment_methods}}
|
||||
<li>
|
||||
<p>{{issuer_name}} {{account_number}} a/n >{{account_name}}</p>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p class="mb0">Once you've made the payment, please kindly email or send the proof of payment so we can proceed with your booking promptly</p>
|
||||
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,400 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
|
||||
down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
Margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
Margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
Margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
Margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
Margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn>tbody>tr>td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
<table class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p>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> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,419 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
|
||||
down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
Margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
Margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
Margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
Margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
Margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn>tbody>tr>td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
<table class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p>Thank you fot choosing us! We're absolutelty thrilled and can't wait to embark on this exciting day with you. See you soon for fun times ahead</p>
|
||||
|
||||
<p class="mb0">Here's a quick recap of your invoice:</p>
|
||||
<p class="mb0">Booking Date: {{booking_date}}</p>
|
||||
<p>Total Invoice: {{payment_total}}</p>
|
||||
|
||||
<p class="mb0">Just a friendly reminder that your invoice will expire on {{expire_date}}</p>
|
||||
<p>To keep things running smoothly, please ensure your payment is completed before this data</p>
|
||||
|
||||
<p class="mb0">For your convenience, here is a list of our account details:</p>
|
||||
<a href="{{payment_midtrans_url}}">{{payment_midtrans_url}}</a>
|
||||
<br><br>
|
||||
|
||||
<p class="mb0">Once you've made the payment, please kindly email or send the proof of payment so we can proceed with your booking promptly</p>
|
||||
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<!-- <div class="footer">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block powered-by">
|
||||
Powered by Skyworld
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div> -->
|
||||
<!-- END FOOTER -->
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,412 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
|
||||
down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
Margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
Margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
Margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
Margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
Margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn>tbody>tr>td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
<table class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p class="mb0">We are excited to inform you that your payment has been successfully received!</p>
|
||||
<p class="mb0">Attached to this email, you will find your confirmatin receipt</p>
|
||||
<p class="mb0">Please keep this safe as you will need to show it at the entrance upon your arrival</p>
|
||||
<p>It's your golden ticket to all the fun and excitement awaiting you!</p>
|
||||
|
||||
<br>
|
||||
<p class="mb0">Here's a quick recap:</p>
|
||||
|
||||
<p class="mb0">Booking Date: {{booking_date}}</p>
|
||||
<p class="mb0">Invoice Code: {{invoice_code}}</p>
|
||||
<p class="mb0">Payment Date: {{payment_date}}</p>
|
||||
<p class="mb0">Payment Code: {{payment_code}}</p>
|
||||
<p class="mb0">Payment Via: {{payment_via}}</p>
|
||||
<p class="mb0">Account No: {{account_no}}</p>
|
||||
<p>On Behalf Of: {{account_name}}</p>
|
||||
<br>
|
||||
|
||||
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
<br>
|
||||
|
||||
<p class="mb0">Font forget to bring a smile and your confirmation receipt (attached) for a smooth entry</p>
|
||||
<p>We can't wait to see you and ensure you have an amazing time with us!</p>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
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
|
@ -0,0 +1,415 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
|
||||
down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
Margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
Margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
Margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
Margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
Margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn>tbody>tr>td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
<table class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p class="mb-0">Good News!</p>
|
||||
<p>We've successfully processed your refund for:</p>
|
||||
|
||||
<p class="mb0">Here are the details of your refund:</p>
|
||||
<p class="mb0">Transaction Date: <b>{{booking_date}}</b></p>
|
||||
<p class="mb0">Transaction Code: <b>{{invoice_code}}</b></p>
|
||||
<p class="mb0">Total Refund: <b>{{refund.refund_total}}</b></p>
|
||||
<p>{{{refund_items}}}</p>
|
||||
|
||||
<p class="mb0">Transaction Number: <b>{{invoice_code}}</b></p>
|
||||
<p class="mb0">Refund Processed Date: <b>{{refund.refund_date}}</b></p>
|
||||
<p class="mb0">Bank Account: <b>{{refund.bank_name}}</b></p>
|
||||
<p class="mb0">Account Number: <b>{{refund.bank_account_number}}</b></p>
|
||||
<p>Account Name: <b>{{refund.bank_account_name}}</b></p>
|
||||
|
||||
<p class="mb0">We hope this helps make things right, and we're here to assist if you need anything else</p>
|
||||
<p>You should see the refund in your account within 3 business days</p>
|
||||
|
||||
|
||||
<p class="mb0">Thank you for your patience and understanding</p>
|
||||
<p>If you have any questions or need further assistance, don't hesitate to reach out us at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
|
||||
<br>
|
||||
|
||||
<p class="mb0">Thank you and we can't wait to see you and make sure you have an amazing time!</p>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,409 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Email Confirmation</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
font-family: sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
|
||||
.body {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
|
||||
down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
Margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
Margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
Margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #000000;
|
||||
font-family: sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
Margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
Margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn>tbody>tr>td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
Margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
ol li {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
<table class="main">
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="mb0"><b>Dear,</b></p>
|
||||
<p class="mb0">{{customer_name}}</p>
|
||||
<p>{{customer_phone}}</p>
|
||||
|
||||
<p class="mb0">We're trully sorry for any inconvenience that led to this request</p>
|
||||
<p>We've received your refund request for :</p>
|
||||
|
||||
<p class="mb0">Transaction Date: <b>{{booking_date}}</b></p>
|
||||
<p class="mb0">Transaction Code: <b>{{invoice_code}}</b></p>
|
||||
<p class="mb0">Refund Code: <b>{{refund.code}}</b></p>
|
||||
<p class="mb0">Total Refund: <b>{{refund.refund_total}}</b></p>
|
||||
<p>{{{refund_items}}}</p>
|
||||
|
||||
<p class="mb0">Your satisfaction is important to us, and we're commited to resolving this as quickly as possible</p>
|
||||
<p class="mb0">Our team is already on it and will process your refund request promptly</p>
|
||||
<p>We'll keep you updated and notify you once the refund has been processed</p>
|
||||
|
||||
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
|
||||
<b>{{phone_cs}}</b>
|
||||
|
||||
<br>
|
||||
|
||||
<p class="mb0">Thank you for your patience and understanding</p>
|
||||
<p>We appriciate your feedback and are here to make things right</p>
|
||||
|
||||
<br><br>
|
||||
<b>Best Regrads,</b><br>
|
||||
<b>WEplayground</b>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
After Width: | Height: | Size: 504 KiB |
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"type": "service_account",
|
||||
"project_id": "weplayground-app",
|
||||
"private_key_id": "e3ed1a4430140ac589c6e9e7ce125d16d8f7304a",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCmEl90K7ojdx1\nnJv5BKq3THI+l+pgC4dlqOuEV4sc2SFXECgyEgEYAFH6U8eH9TTl5wW5hFhvpiWl\nHSxZA2nMa1ojp97mkufzaGgsJbcB4ni9ydoJyN9Hqs2Wz+JiBtjscGOrmOP1bNyn\nBO9RhHInh0bfTNMrtsIicr4DPNIfM2sl95v6pCDGt7Cfu2NoEnDhId50d73KVONI\n0+rf90WhehEMwoZEzYI0gLmSVbnPEm1j4/OOQfQl7FjaFKyle+A5BWaiRsIqiSue\n0jvZz0DlGmjeHx1yjBIKpq5omOku7aYi4kTNEZKKxzs5HhRFKi2KuYNK/WD5ApQg\ncIhGhhCfAgMBAAECggEANX+LmNjh9VJm/Tigkt4LFxifwgCe8WfKAhNmKHyu5K/3\nIAnzmwxjG5ee8gzNat3pfJk+dCnj7FIHwHScSB6NnCMZZXsV51sVBNC77wMxZIXA\nPyE63fzJEdlt6xvc96k9QweFB1yhs0wJ/6r2JnmcrqxcujBTUA3PIoxcG+TBOc08\ndo5Rcbeq6/3txjGlFM1820WViuFSQQiL6PgNVb+l0JrQ8rAOflKYFOkUb8wux9LX\nnD4vJMwa0j+GRvH5BCcZCguIQZn2JR3rTgcavWtcaHiTNsc49Lsj/hGGOsbkFROo\nGWaSgXE169xiVR/MMEblzqpSXq1qXF2iUeaqyUFIZQKBgQDxxrNlDs1qMfcaQ0S2\nVVtU/f1NfY+kCjQaC4CoYJaaoZINs5ODPs8/2DGnHuhNXMtnPeQ+SzNaK1e1eLbw\nmvq1+n3aGZTvUq2L3b+v7JJ6TQmQ4eBLZBzNjxrxC3EkCULTuROtsAhfzORuE0mE\nwnhR5LpPraEBrPi0re9yDDXVHQKBgQDOCwGw1gNVLh622qR65Zhx5rs2q6ktPxq2\neiUV0KDug6/7QbJzg1pNeoVQmadJR86H0fzKMsN5C7t7z3MIkqXc0+T1NmdN2fPm\ndLthnR1grCDYykoet/CITbAfiip27/o3TJ7YIYItefyZ4GnNH82R/4z3LBDnXB9f\n565hbUj76wKBgEnNMpOFijSBXgFZSU8zDPcLtNeDnWYgazkMC9DZ8v7ulOuzxjKI\n6LB/aOCvsY9z5O712IcfY2SB2HsfhxA47pDADsyVhH3tSeZo4QttdmT4wRPFrza0\nL4qbxUiRCo9KeGiylQwusM+1doEXSBjLV/j/jdOml4AwcZaNhYrVqVUNAoGAU0uD\nzXdXNZJFfGp7X+t9a155hKp05APEyswqPd1vkbzO4eY3PBd35CaJyoGzbR6IUcQE\nS8Gl4ENr8at1t5uBTfqjbrYloQVhYmMCdX3MqI4tYTa2LCD0LkYp0zZJ4Hc3Ui+5\nb2psc/ICujpMy032DvWeiTXZR46oaF8C0gQaIy0CgYEAmKCP4CXmPlWoWqebFp3W\nz2eKWUfASioQ+ZGUVNEge4a6iutciydQJZxBfg9ZXWqDfI0FoRSPfs2zUZFO0AcM\n6oaPGiFnTnH8FGcSHu3p0YysevyoSY6tgsAhb3IiKjJd4e7btsYzpPZbIfyfUVHK\nQFOOSkE+x4J5ts+XO6isQ+w=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "weplayground@weplayground-app.iam.gserviceaccount.com",
|
||||
"client_id": "106351339097550564510",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/weplayground%40weplayground-app.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
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"
|
|
@ -0,0 +1,49 @@
|
|||
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
|
|
@ -0,0 +1,46 @@
|
|||
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
|
|
@ -0,0 +1,42 @@
|
|||
## 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)
|
||||
}
|
||||
```
|
34
package.json
34
package.json
|
@ -17,26 +17,52 @@
|
|||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"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": {
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@nestjs/axios": "^3.0.3",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.2",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/cqrs": "^10.2.7",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/schedule": "^4.1.0",
|
||||
"@nestjs/swagger": "^7.3.1",
|
||||
"@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-validator": "^0.14.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"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",
|
||||
"nodemailer": "^6.9.14",
|
||||
"pdfmake": "^0.2.10",
|
||||
"pg": "^8.11.5",
|
||||
"plop": "^4.0.1",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.5.0",
|
||||
"typeorm": "^0.3.20"
|
||||
"typeorm": "^0.3.20",
|
||||
"typeorm-seeding": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
|
@ -44,7 +70,7 @@
|
|||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node": "^20.12.13",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
|
@ -57,7 +83,7 @@
|
|||
"supertest": "^6.1.3",
|
||||
"ts-jest": "29.0.3",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "4.1.1",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,246 @@
|
|||
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;
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
import { Module, Scope } from '@nestjs/common';
|
||||
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 { HttpExceptionFilter, TransformInterceptor } from './core/response';
|
||||
import { ApmModule } from './core/apm';
|
||||
|
@ -9,12 +7,96 @@ import { CONNECTION_NAME } from './core/strings/constants/base.constants';
|
|||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
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 { 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({
|
||||
imports: [
|
||||
ApmModule.register(),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
|
@ -27,19 +109,116 @@ import { CouchModule } from './modules/configuration/couch/couch.module';
|
|||
password: process.env.DEFAULT_DB_PASS,
|
||||
database: process.env.DEFAULT_DB_NAME,
|
||||
entities: [
|
||||
UserPrivilegeModel,
|
||||
],
|
||||
synchronize: true,
|
||||
}),
|
||||
CqrsModule,
|
||||
SessionModule,
|
||||
AuthModule,
|
||||
CouchModule,
|
||||
...UserPrivilegeModels,
|
||||
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,
|
||||
}),
|
||||
AuthModule,
|
||||
ConstantModule,
|
||||
CqrsModule,
|
||||
CouchModule,
|
||||
CronModule,
|
||||
ExportModule,
|
||||
GoogleCalendarModule,
|
||||
LogModule,
|
||||
MailModule,
|
||||
MidtransModule,
|
||||
SessionModule,
|
||||
UploadModule,
|
||||
|
||||
// user
|
||||
TenantModule,
|
||||
UserModule,
|
||||
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: [],
|
||||
providers: [
|
||||
AuthService,
|
||||
PrivilegeService,
|
||||
/**
|
||||
* By default all request from client will protect by JWT
|
||||
* if there is some endpoint/function that does'nt require authentication
|
||||
|
@ -48,7 +227,7 @@ import { CouchModule } from './modules/configuration/couch/couch.module';
|
|||
{
|
||||
provide: APP_GUARD,
|
||||
scope: Scope.REQUEST,
|
||||
useClass: JWTGuard,
|
||||
useClass: RolesGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
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 {}
|
|
@ -1,32 +0,0 @@
|
|||
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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
password: string;
|
||||
roles: string[];
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
export const UNPROTECTED_URL = 'unprotected_url';
|
||||
export const PRIVILEGE_KEY = 'privilege_key';
|
||||
export const MAIN_MENU = 'main_menu';
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { SetMetadata } from '@nestjs/common';
|
||||
import { UNPROTECTED_URL } from '../../constants';
|
||||
import { MAIN_MENU, UNPROTECTED_URL } from '../../constants';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use Public instead
|
||||
* This decorator will exclude the request from token check
|
||||
*
|
||||
* NOTE:
|
||||
|
@ -11,3 +13,9 @@ import { UNPROTECTED_URL } from '../../constants';
|
|||
*/
|
||||
export const Unprotected = (isUnprotected = true) =>
|
||||
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);
|
||||
|
|
|
@ -2,27 +2,29 @@ import {
|
|||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
Scope,
|
||||
Logger,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SessionService, UsersSession } from 'src/core/sessions';
|
||||
import { UNPROTECTED_URL } from '../constants';
|
||||
import { PrivilegeService } from './services/privilege.service';
|
||||
import { AuthService } from './services/auth.service';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class JWTGuard implements CanActivate {
|
||||
constructor(
|
||||
protected readonly session: SessionService,
|
||||
protected readonly reflector: Reflector,
|
||||
protected readonly privilege: PrivilegeService,
|
||||
protected readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
protected isPublic = false;
|
||||
protected userSession: UsersSession;
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
async canActivate(context: ExecutionContext) {
|
||||
/**
|
||||
* Check if access url is protected or not
|
||||
* By default `isUnprotected` equals `false`
|
||||
|
@ -31,6 +33,8 @@ export class JWTGuard implements CanActivate {
|
|||
UNPROTECTED_URL,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
this.isPublic = isUnprotected;
|
||||
this.session.setPublic(isUnprotected);
|
||||
if (isUnprotected) return true;
|
||||
|
||||
/**
|
||||
|
@ -56,9 +60,29 @@ export class JWTGuard implements CanActivate {
|
|||
*/
|
||||
try {
|
||||
this.userSession = this.session.verifyToken(token);
|
||||
await this.authService.verifyRegisteredLoginToken(token);
|
||||
|
||||
Logger.log(`Access from ${this.userSession.name}`, 'AuthGuard');
|
||||
return true;
|
||||
} 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({
|
||||
code: 10001,
|
||||
message:
|
||||
|
|
|
@ -1,23 +1,36 @@
|
|||
import { Injectable, ExecutionContext } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
Injectable,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { JWTGuard } from './jwt.guard';
|
||||
import { MAIN_MENU } from '../constants';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard extends JWTGuard {
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
super.canActivate(context);
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
await super.canActivate(context);
|
||||
|
||||
/**
|
||||
* Create function to check if `this.userSession` have access
|
||||
* to Read / Create / Update / and Other Action
|
||||
*/
|
||||
// jika endpoint tersebut bukan public, maka lakukan check lanjutan
|
||||
if (!this.isPublic) {
|
||||
// Check apakah endpoint ada decorator untuk exlude privilege (@ExcludePrivilege())
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
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',
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
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;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
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';
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
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`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
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,
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
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);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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}%`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,12 +1,6 @@
|
|||
import { Param } from 'src/core/modules/domain/entities/base-filter.entity';
|
||||
import { Brackets, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
export interface Param {
|
||||
cols: string;
|
||||
data: string[];
|
||||
additional?: any[];
|
||||
leftJoin?: any[];
|
||||
}
|
||||
|
||||
export class SpecificSearchFilter<Entity = any> {
|
||||
constructor(
|
||||
private query: SelectQueryBuilder<Entity>,
|
||||
|
@ -26,12 +20,16 @@ export class SpecificSearchFilter<Entity = any> {
|
|||
new Brackets((qb) => {
|
||||
params.forEach((param) => {
|
||||
const { cols, data, additional, leftJoin } = param;
|
||||
const columns = cols.split('.');
|
||||
let arr = data;
|
||||
|
||||
const arr = data?.map((el) =>
|
||||
if (!columns.includes('status::text')) {
|
||||
arr = data?.map((el) =>
|
||||
el.includes("'")
|
||||
? `'%${el.trim().replace(/'/g, "''").replace(/\s+/g, ' ')}%'`
|
||||
: `'%${el.trim().replace(/\s+/g, ' ')}%'`,
|
||||
);
|
||||
}
|
||||
|
||||
const aliases = !cols.match(/\./g)
|
||||
? this.table.concat(`.${cols}`)
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { BaseCoreEntity } from "../../domain/entities/base-core.entity";
|
||||
import { Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { BaseCoreEntity } from '../../domain/entities/base-core.entity';
|
||||
|
||||
@Entity()
|
||||
export abstract class BaseCoreModel<Entity> implements BaseCoreEntity {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { Column, Entity } from "typeorm";
|
||||
import { BaseModel } from "./base.model";
|
||||
import { STATUS } from "src/core/strings/constants/base.constants";
|
||||
import { BaseStatusEntity } from "../../domain/entities/base-status.entity";
|
||||
import { Column, Entity } from 'typeorm';
|
||||
import { BaseModel } from './base.model';
|
||||
import { STATUS } from 'src/core/strings/constants/base.constants';
|
||||
import { BaseStatusEntity } from '../../domain/entities/base-status.entity';
|
||||
|
||||
@Entity()
|
||||
export abstract class BaseStatusModel<Entity> extends BaseModel<Entity> implements BaseStatusEntity {
|
||||
export abstract class BaseStatusModel<Entity>
|
||||
extends BaseModel<Entity>
|
||||
implements BaseStatusEntity
|
||||
{
|
||||
@Column('enum', { name: 'status', enum: STATUS, default: STATUS.DRAFT })
|
||||
status: STATUS;
|
||||
}
|
|
@ -3,7 +3,10 @@ import { BaseCoreModel } from './base-core.model';
|
|||
import { BaseEntity } from '../../domain/entities/base.entity';
|
||||
|
||||
@Entity()
|
||||
export abstract class BaseModel<Entity> extends BaseCoreModel<Entity> implements BaseEntity {
|
||||
export abstract class BaseModel<Entity>
|
||||
extends BaseCoreModel<Entity>
|
||||
implements BaseEntity
|
||||
{
|
||||
@Column('varchar', { name: 'creator_id', length: 36, nullable: true })
|
||||
creator_id: string;
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { EntityTarget, FindManyOptions, QueryRunner, Repository } from "typeorm";
|
||||
import {
|
||||
EntityTarget,
|
||||
FindManyOptions,
|
||||
QueryRunner,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
|
||||
export abstract class BaseDataService<Entity> {
|
||||
|
||||
constructor(private repository: Repository<Entity>) {}
|
||||
|
||||
getRepository(): Repository<Entity> {
|
||||
|
@ -13,8 +17,26 @@ export abstract class BaseDataService<Entity> {
|
|||
entityTarget: EntityTarget<Entity>,
|
||||
entity: Entity,
|
||||
): Promise<Entity> {
|
||||
const newEntity = queryRunner.manager.create(entityTarget, entity);
|
||||
return await queryRunner.manager.save(newEntity);
|
||||
// const newEntity = this.repository.create(entityTarget, entity);
|
||||
return await this.repository.save(entity);
|
||||
}
|
||||
|
||||
async createMany(
|
||||
queryRunner: QueryRunner,
|
||||
entityTarget: EntityTarget<Entity>,
|
||||
entity: Entity[],
|
||||
): Promise<Entity[]> {
|
||||
// const newEntity = this.repository.create(entityTarget, entity);
|
||||
return await this.repository.save(entity);
|
||||
}
|
||||
|
||||
async createBatch(
|
||||
queryRunner: QueryRunner,
|
||||
entityTarget: EntityTarget<Entity>,
|
||||
entity: Entity[],
|
||||
): Promise<Entity[]> {
|
||||
// const newEntity = this.repository.create(entityTarget, entity);
|
||||
return await this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
|
@ -23,13 +45,13 @@ export abstract class BaseDataService<Entity> {
|
|||
filterUpdate: any,
|
||||
entity: Entity,
|
||||
): Promise<Entity> {
|
||||
const newEntity = await queryRunner.manager.findOne(entityTarget, {
|
||||
const newEntity = await this.repository.findOne({
|
||||
where: filterUpdate,
|
||||
});
|
||||
|
||||
if (!newEntity) throw new Error('Data not found!');
|
||||
Object.assign(newEntity, entity);
|
||||
return await queryRunner.manager.save(newEntity);
|
||||
return await this.repository.save(newEntity);
|
||||
}
|
||||
|
||||
async deleteById(
|
||||
|
@ -37,7 +59,15 @@ export abstract class BaseDataService<Entity> {
|
|||
entityTarget: EntityTarget<Entity>,
|
||||
id: string,
|
||||
): Promise<void> {
|
||||
await queryRunner.manager.delete(entityTarget, { id });
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
async deleteByIds(
|
||||
queryRunner: QueryRunner,
|
||||
entityTarget: EntityTarget<Entity>,
|
||||
ids: string[],
|
||||
): Promise<void> {
|
||||
await this.repository.delete(ids);
|
||||
}
|
||||
|
||||
async deleteByOptions(
|
||||
|
@ -45,10 +75,15 @@ export abstract class BaseDataService<Entity> {
|
|||
entityTarget: EntityTarget<Entity>,
|
||||
findManyOptions: FindManyOptions<Entity>,
|
||||
): Promise<void> {
|
||||
await queryRunner.manager.delete(entityTarget, findManyOptions);
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
import { FindOneOptions, Repository, SelectQueryBuilder } from "typeorm";
|
||||
import { BaseFilterEntity } from "../../domain/entities/base-filter.entity";
|
||||
import { PaginationResponse } from "src/core/response/domain/ok-response.interface";
|
||||
import { FindOneOptions, Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import { BaseFilterEntity } from '../../domain/entities/base-filter.entity';
|
||||
import { PaginationResponse } from 'src/core/response/domain/ok-response.interface';
|
||||
|
||||
export abstract class BaseReadService<Entity> {
|
||||
|
||||
constructor(private repository: Repository<Entity>) {}
|
||||
|
||||
getRepository(): Repository<Entity> {
|
||||
|
@ -14,9 +13,11 @@ export abstract class BaseReadService<Entity> {
|
|||
queryBuilder: SelectQueryBuilder<Entity>,
|
||||
params: BaseFilterEntity,
|
||||
): Promise<PaginationResponse<Entity>> {
|
||||
const limit = params.limit ?? 10;
|
||||
const page = params.page ?? 1;
|
||||
const [data, total] = await queryBuilder
|
||||
.take(+params.limit)
|
||||
.skip(+params.limit * +params.page - +params.limit)
|
||||
.take(+limit)
|
||||
.skip(+limit * +page - +limit)
|
||||
.getManyAndCount();
|
||||
|
||||
return {
|
||||
|
@ -38,5 +39,4 @@ export abstract class BaseReadService<Entity> {
|
|||
async getManyByOptions(findManyOptions): Promise<Entity[]> {
|
||||
return await this.repository.find(findManyOptions);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,50 +1,39 @@
|
|||
import { Repository, TreeRepository } from "typeorm";
|
||||
import { BaseReadService } from "./base-read.service";
|
||||
|
||||
export abstract class BaseTreeReadService<Entity> extends BaseReadService<Entity> {
|
||||
import { Repository, TreeRepository } from 'typeorm';
|
||||
import { BaseReadService } from './base-read.service';
|
||||
|
||||
export abstract class BaseTreeReadService<
|
||||
Entity,
|
||||
> extends BaseReadService<Entity> {
|
||||
constructor(
|
||||
private dataRepository: Repository<Entity>,
|
||||
private treeRepository: TreeRepository<Entity>
|
||||
private treeRepository: TreeRepository<Entity>,
|
||||
) {
|
||||
super(dataRepository);
|
||||
}
|
||||
|
||||
async findRoots() {
|
||||
return this.treeRepository.findRoots()
|
||||
return this.treeRepository.findRoots();
|
||||
}
|
||||
|
||||
async findDescendants(
|
||||
parent,
|
||||
relations = [],
|
||||
): Promise<Entity[]> {
|
||||
async findDescendants(parent, relations = []): Promise<Entity[]> {
|
||||
return this.treeRepository.findDescendants(parent, {
|
||||
relations: relations,
|
||||
});
|
||||
}
|
||||
|
||||
async findDescendantsTree(
|
||||
parent,
|
||||
relations = [],
|
||||
): Promise<Entity> {
|
||||
async findDescendantsTree(parent, relations = []): Promise<Entity> {
|
||||
return this.treeRepository.findDescendantsTree(parent, {
|
||||
relations: relations,
|
||||
});
|
||||
}
|
||||
|
||||
async findAncestors(
|
||||
parent,
|
||||
relations = [],
|
||||
): Promise<Entity[]> {
|
||||
async findAncestors(parent, relations = []): Promise<Entity[]> {
|
||||
return await this.treeRepository.findAncestors(parent, {
|
||||
relations: relations,
|
||||
});
|
||||
}
|
||||
|
||||
async findAncestorsTree(
|
||||
parent,
|
||||
relations = [],
|
||||
): Promise<Entity> {
|
||||
async findAncestorsTree(parent, relations = []): Promise<Entity> {
|
||||
return await this.treeRepository.findAncestorsTree(parent, {
|
||||
relations: relations,
|
||||
});
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export interface BaseCoreEntity {
|
||||
id: string;
|
||||
id?: string;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
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 {
|
||||
page: number;
|
||||
|
@ -16,3 +16,17 @@ export interface BaseFilterEntity {
|
|||
updated_from: 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[];
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { STATUS } from "src/core/strings/constants/base.constants";
|
||||
import { BaseEntity } from "./base.entity";
|
||||
import { STATUS } from 'src/core/strings/constants/base.constants';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
export interface BaseStatusEntity extends BaseEntity {
|
||||
status: STATUS;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseCoreEntity } from "./base-core.entity";
|
||||
import { BaseCoreEntity } from './base-core.entity';
|
||||
|
||||
export interface BaseEntity extends BaseCoreEntity {
|
||||
creator_id: string;
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { Inject, Injectable, Logger } from "@nestjs/common";
|
||||
import { UserProvider, UsersSession } from "src/core/sessions";
|
||||
import { BLANK_USER } from "src/core/strings/constants/base.constants";
|
||||
import { TABLE_NAME } from "src/core/strings/constants/table.constants";
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { UserProvider, UsersSession } from 'src/core/sessions';
|
||||
import { BLANK_USER } from 'src/core/strings/constants/base.constants';
|
||||
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
|
||||
import { RelationParam } from '../entities/base-filter.entity';
|
||||
|
||||
@Injectable()
|
||||
export abstract class BaseReadManager {
|
||||
|
||||
public user: UsersSession;
|
||||
public dataService: any;
|
||||
public queryBuilder: any;
|
||||
protected tableName: TABLE_NAME;
|
||||
abstract get relations(): RelationParam;
|
||||
abstract get selects(): string[];
|
||||
@Inject()
|
||||
protected userProvider: UserProvider;
|
||||
|
||||
|
@ -23,9 +25,12 @@ export abstract class BaseReadManager {
|
|||
}
|
||||
}
|
||||
|
||||
setService(dataService) {
|
||||
setService(dataService, tableName) {
|
||||
this.dataService = dataService;
|
||||
this.queryBuilder = this.dataService.getRepository().createQueryBuilder(this.tableName);
|
||||
this.tableName = tableName;
|
||||
this.queryBuilder = this.dataService
|
||||
.getRepository()
|
||||
.createQueryBuilder(this.tableName);
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
|
|
|
@ -1,34 +1,57 @@
|
|||
import { BadRequestException, Inject, Injectable, Logger } from "@nestjs/common";
|
||||
import { EventBus } from "@nestjs/cqrs";
|
||||
import { UserProvider, UsersSession } from "src/core/sessions";
|
||||
import { BLANK_USER } from "src/core/strings/constants/base.constants";
|
||||
import { EventTopics } from "src/core/strings/constants/interface.constants";
|
||||
import { QueryRunner } from "typeorm";
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { EventBus } from '@nestjs/cqrs';
|
||||
import { UserProvider, UsersSession } from 'src/core/sessions';
|
||||
import { BLANK_USER } from 'src/core/strings/constants/base.constants';
|
||||
import {
|
||||
EventTopics,
|
||||
validateRelations,
|
||||
} from 'src/core/strings/constants/interface.constants';
|
||||
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export abstract class BaseManager {
|
||||
public user: UsersSession;
|
||||
public queryRunner: QueryRunner;
|
||||
public dataService: any;
|
||||
protected data: any;
|
||||
public queryRunner: QueryRunner;
|
||||
protected tableName: TABLE_NAME;
|
||||
@Inject()
|
||||
protected userProvider: UserProvider;
|
||||
@Inject()
|
||||
protected eventBus: EventBus;
|
||||
abstract get validateRelations(): validateRelations[];
|
||||
|
||||
// sebagai optional yang dapat digunakan
|
||||
public dataServiceFirstOpt: any;
|
||||
|
||||
// sebagai optional yang dapat digunakan
|
||||
public dataServiceSecondOpt: any;
|
||||
|
||||
private readonly baseLog = new Logger(BaseManager.name);
|
||||
|
||||
setUser() {
|
||||
try {
|
||||
this.user = this.userProvider?.user;
|
||||
this.user = this.userProvider?.user ?? BLANK_USER;
|
||||
} catch (error) {
|
||||
this.user = BLANK_USER;
|
||||
}
|
||||
}
|
||||
|
||||
setService(dataService) {
|
||||
setService(
|
||||
dataService,
|
||||
tableName,
|
||||
dataServiceOpt = null,
|
||||
dataServiceSecondOpt = null,
|
||||
) {
|
||||
this.dataService = dataService;
|
||||
this.queryRunner = this.dataService.getRepository().manager.connection.createQueryRunner();
|
||||
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[];
|
||||
|
@ -41,8 +64,8 @@ export abstract class BaseManager {
|
|||
this.baseLog.verbose('prepareData');
|
||||
await this.prepareData();
|
||||
|
||||
if (!this.data || !this.dataService) {
|
||||
throw new Error("data or service not implemented.");
|
||||
if (!this.dataService) {
|
||||
throw new Error('data or service not implemented.');
|
||||
}
|
||||
|
||||
this.baseLog.verbose('validateProcess');
|
||||
|
@ -59,13 +82,11 @@ export abstract class BaseManager {
|
|||
|
||||
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);
|
||||
} finally {
|
||||
await this.queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,8 +99,4 @@ export abstract class BaseManager {
|
|||
abstract process(): Promise<void>;
|
||||
|
||||
abstract afterProcess(): Promise<void>;
|
||||
|
||||
async publishEvents() {
|
||||
if (!this.eventTopics.length) return
|
||||
};
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
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,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
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,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
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}`;
|
||||
}
|
||||
}
|
|
@ -1,17 +1,27 @@
|
|||
import { BaseManager } from "../base.manager";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { CheckDuplicateHelper } from 'src/core/helpers/query/check-duplicate.helpers';
|
||||
import { BaseManager } from '../base.manager';
|
||||
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 {
|
||||
|
||||
protected result: Entity;
|
||||
protected duplicateColumn: string[];
|
||||
abstract get entityTarget(): any;
|
||||
abstract get uniqueColumns(): columnUniques[];
|
||||
|
||||
setData(entity: Entity): void {
|
||||
this.data = entity;
|
||||
}
|
||||
|
||||
get validateRelations(): validateRelations[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
async prepareData(): Promise<void> {
|
||||
Object.assign(this.data, {
|
||||
creator_id: this.user.id,
|
||||
|
@ -21,20 +31,81 @@ export abstract class BaseCreateManager<Entity> extends BaseManager {
|
|||
});
|
||||
}
|
||||
|
||||
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(
|
||||
this.queryRunner,
|
||||
this.entityTarget,
|
||||
this.data,
|
||||
);
|
||||
|
||||
this.publishEvents();
|
||||
}
|
||||
|
||||
async getResult(): Promise<Entity> {
|
||||
return await this.dataService.getOneByOptions({
|
||||
where: {
|
||||
id: this.result['id']
|
||||
}
|
||||
})
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
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,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { HttpStatus, Injectable, UnauthorizedException, UnprocessableEntityException } from "@nestjs/common";
|
||||
import { BaseManager } from "../base.manager";
|
||||
import { HttpStatus, UnprocessableEntityException } from '@nestjs/common';
|
||||
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 {
|
||||
|
||||
protected dataId: string;
|
||||
protected result: Entity;
|
||||
abstract get entityTarget(): any;
|
||||
|
@ -15,14 +16,14 @@ export abstract class BaseDeleteManager<Entity> extends BaseManager {
|
|||
async prepareData(): Promise<void> {
|
||||
this.data = await this.dataService.getOneByOptions({
|
||||
where: {
|
||||
id: this.dataId
|
||||
}
|
||||
})
|
||||
id: this.dataId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!this.data)
|
||||
throw new UnprocessableEntityException({
|
||||
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
message: `Data with id ${this.dataId} not found`,
|
||||
message: `Gagal! Data denga id ${this.dataId} tidak ditemukan`,
|
||||
error: 'Unprocessable Entity',
|
||||
});
|
||||
|
||||
|
@ -30,12 +31,50 @@ export abstract class BaseDeleteManager<Entity> extends BaseManager {
|
|||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,9 @@
|
|||
import { BaseReadManager } from "../base-read.manager";
|
||||
import { joinRelationHelper } from 'src/core/helpers/query/join-relations.helper';
|
||||
import { BaseReadManager } from '../base-read.manager';
|
||||
|
||||
export abstract class BaseDetailManager<Entity> extends BaseReadManager {
|
||||
|
||||
protected dataId: string;
|
||||
protected result: Entity;
|
||||
|
||||
abstract get selectData(): string[];
|
||||
abstract get relationData(): string[];
|
||||
abstract get setFindProperties(): any;
|
||||
|
||||
setData(dataId: string): void {
|
||||
|
@ -14,7 +11,26 @@ export abstract class BaseDetailManager<Entity> extends BaseReadManager {
|
|||
}
|
||||
|
||||
async process(): Promise<void> {
|
||||
this.queryBuilder.select(this.selectData).where(this.setFindProperties);
|
||||
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);
|
||||
this.queryBuilder.where(this.setFindProperties);
|
||||
this.result = await this.queryBuilder.getOne();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,36 +1,76 @@
|
|||
import { PaginationResponse } from "src/core/response/domain/ok-response.interface";
|
||||
import { BaseReadManager } from "../base-read.manager";
|
||||
import { SelectQueryBuilder } from "typeorm";
|
||||
import { BaseFilterEntity } from "../../entities/base-filter.entity";
|
||||
import { Param, SpecificSearchFilter } from "src/core/helpers/query/specific-search.helper";
|
||||
import { PaginationResponse } from 'src/core/response/domain/ok-response.interface';
|
||||
import { BaseReadManager } from '../base-read.manager';
|
||||
import { SelectQueryBuilder } from 'typeorm';
|
||||
import { SpecificSearchFilter } from 'src/core/helpers/query/specific-search.helper';
|
||||
import {
|
||||
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 {
|
||||
|
||||
protected result: PaginationResponse<Entity>;
|
||||
public filterParam: BaseFilterEntity;
|
||||
public filterParam: any;
|
||||
abstract get specificFilter(): Param[];
|
||||
|
||||
setFilterParam(param: BaseFilterEntity): void {
|
||||
setFilterParam(param: any): void {
|
||||
this.filterParam = param;
|
||||
}
|
||||
|
||||
async process(): Promise<void> {
|
||||
// const filterSearch: string[] = this.setFilterSearch();
|
||||
const specificFilter = this.specificFilter;
|
||||
const { joinRelations, selectRelations, countRelations } = this.relations;
|
||||
|
||||
// this.queryBuilder.andWhere(
|
||||
// new Brackets((qb) => {
|
||||
// filterSearch.map((fSearch) => {
|
||||
// qb.orWhere(`${fSearch} ILIKE :query`, {
|
||||
// query: `%${
|
||||
// this.filterParam.q.trim().replace(/\s+/g, ' ') ?? ''
|
||||
// }%`,
|
||||
// });
|
||||
// });
|
||||
// }),
|
||||
// );
|
||||
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',
|
||||
);
|
||||
|
||||
new SpecificSearchFilter<Entity>(this.queryBuilder, this.tableName, this.specificFilter).getFilter();
|
||||
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>(
|
||||
this.queryBuilder,
|
||||
this.tableName,
|
||||
specificFilter,
|
||||
).getFilter();
|
||||
|
||||
getOrderBy(this.filterParam, this.queryBuilder, this.tableName);
|
||||
this.setQueryFilter(this.queryBuilder);
|
||||
setQueryFilterDefault(this.queryBuilder, this.filterParam, this.tableName);
|
||||
|
||||
this.result = await this.dataService.getIndex(
|
||||
this.queryBuilder,
|
||||
|
@ -38,16 +78,10 @@ export abstract class BaseIndexManager<Entity> extends BaseReadManager {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
setFilterSearch(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
abstract setQueryFilter(
|
||||
queryBuilder: SelectQueryBuilder<Entity>,
|
||||
): SelectQueryBuilder<Entity>;
|
||||
|
||||
|
||||
getResult(): PaginationResponse<Entity> {
|
||||
return this.result;
|
||||
}
|
||||
|
|
|
@ -1,42 +1,126 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { BaseManager } from "../base.manager";
|
||||
import { STATUS } from "src/core/strings/constants/base.constants";
|
||||
import { UserPrivilegeModel } from "src/modules/user-related/user-privilege/data/model/user-privilege.model";
|
||||
import { ValidateRelationHelper } from 'src/core/helpers/validation/validate-relation.helper';
|
||||
import { BaseManager } from '../base.manager';
|
||||
import {
|
||||
OPERATION,
|
||||
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 {
|
||||
|
||||
protected dataId: string;
|
||||
protected result: Entity;
|
||||
protected dataStatus: STATUS;
|
||||
protected oldData: Entity;
|
||||
protected dataStatus: STATUS | QUEUE_STATUS;
|
||||
protected relations = [];
|
||||
protected duplicateColumn: string[];
|
||||
abstract get entityTarget(): any;
|
||||
|
||||
setData(id: string, status: STATUS): void {
|
||||
setData(id: string, status: STATUS | QUEUE_STATUS): void {
|
||||
/**
|
||||
* // TODO: Handle case confirm multiple tabs;
|
||||
* Pola id yang dikirim dirubah menjadi data_id___updated_at
|
||||
* Untuk mendapatkan value id nya saja dan menghindari breaking change
|
||||
* karena sudah digunakan sebelumnya lakukan split dari data ids yang dikirim
|
||||
* Example:
|
||||
* this.dataId = id.split('___')[0]
|
||||
*
|
||||
* Simpan data id yang mempunyai update_at kedalam valiable baru
|
||||
* Example:
|
||||
* this.dataIdsWithDate = {
|
||||
* id: id.split('___')[0],
|
||||
* updated_at: id.split('___')[1]
|
||||
* }
|
||||
*/
|
||||
|
||||
this.dataId = id;
|
||||
this.dataStatus = status;
|
||||
}
|
||||
|
||||
async prepareData(): Promise<void> {
|
||||
this.data = new UserPrivilegeModel();
|
||||
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(),
|
||||
id: this.dataId,
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,18 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { BaseManager } from "../base.manager";
|
||||
import { CheckDuplicateHelper } from 'src/core/helpers/query/check-duplicate.helpers';
|
||||
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 {
|
||||
|
||||
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 {
|
||||
this.dataId = id;
|
||||
|
@ -15,27 +20,97 @@ export abstract class BaseUpdateManager<Entity> extends BaseManager {
|
|||
}
|
||||
|
||||
async prepareData(): Promise<void> {
|
||||
this.oldData = await this.dataService.getOneByOptions({
|
||||
where: { id: this.dataId },
|
||||
});
|
||||
|
||||
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, {
|
||||
editor_id: this.user.id,
|
||||
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
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,7 +1,13 @@
|
|||
import { BaseDataOrchestrator } from "./base-data.orchestrator";
|
||||
import { BatchResult } from 'src/core/response/domain/ok-response.interface';
|
||||
import { BaseDataOrchestrator } from './base-data.orchestrator';
|
||||
|
||||
export abstract class BaseDataTransactionOrchestrator<Entity> extends BaseDataOrchestrator<Entity> {
|
||||
abstract active(dataId: string): Promise<String>;
|
||||
abstract confirm(dataId: string): Promise<String>;
|
||||
abstract inactive(dataId: string): Promise<String>;
|
||||
export abstract class BaseDataTransactionOrchestrator<
|
||||
Entity,
|
||||
> extends BaseDataOrchestrator<Entity> {
|
||||
abstract active(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>;
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
export abstract class BaseDataOrchestrator<Entity> {
|
||||
import { BatchResult } from 'src/core/response/domain/ok-response.interface';
|
||||
|
||||
export abstract class BaseDataOrchestrator<Entity> {
|
||||
abstract create(data: Entity): Promise<Entity>;
|
||||
abstract update(dataId: string, data: Entity): Promise<Entity>;
|
||||
abstract delete(dataId: string): Promise<String>;
|
||||
|
||||
abstract delete(dataId: string): Promise<string>;
|
||||
abstract batchDelete(dataIds: string[]): Promise<BatchResult>;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
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> {
|
||||
abstract index(params): Promise<PaginationResponse<Entity>>;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class BatchIdsDto {
|
||||
@ApiProperty({ type: [String] })
|
||||
ids: string[];
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export class ChangePositionDto {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseCoreEntity } from "../../domain/entities/base-core.entity";
|
||||
import { BaseCoreEntity } from '../../domain/entities/base-core.entity';
|
||||
|
||||
export class BaseCoreDto implements BaseCoreEntity {
|
||||
id: string;
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { BaseFilterEntity } from "../../domain/entities/base-filter.entity";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsArray, IsEnum, IsNumber, IsString, ValidateIf } from "class-validator";
|
||||
import { ORDER_TYPE, STATUS } from "src/core/strings/constants/base.constants";
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BaseFilterEntity } from '../../domain/entities/base-filter.entity';
|
||||
import { Transform } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsString,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
import { ORDER_TYPE, STATUS } from 'src/core/strings/constants/base.constants';
|
||||
|
||||
export class BaseFilterDto implements BaseFilterEntity {
|
||||
@ApiProperty({ type: Number, required: false, default: 1 })
|
||||
|
@ -55,12 +61,6 @@ export class BaseFilterDto implements BaseFilterEntity {
|
|||
})
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsEnum(STATUS, {
|
||||
message: `Status must be a valid enum ${JSON.stringify(
|
||||
Object.values(STATUS),
|
||||
)}`,
|
||||
each: true,
|
||||
})
|
||||
statuses: STATUS[];
|
||||
|
||||
@ApiProperty({ type: [String], required: false })
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { STATUS } from "src/core/strings/constants/base.constants";
|
||||
import { BaseStatusEntity } from "../../domain/entities/base-status.entity";
|
||||
import { BaseDto } from "./base.dto";
|
||||
import { STATUS } from 'src/core/strings/constants/base.constants';
|
||||
import { BaseStatusEntity } from '../../domain/entities/base-status.entity';
|
||||
import { BaseDto } from './base.dto';
|
||||
|
||||
export class BaseStatusDto extends BaseDto implements BaseStatusEntity {
|
||||
status: STATUS;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BaseEntity } from "../../domain/entities/base.entity";
|
||||
import { BaseCoreDto } from "./base-core.dto";
|
||||
import { BaseEntity } from '../../domain/entities/base.entity';
|
||||
import { BaseCoreDto } from './base-core.dto';
|
||||
|
||||
export class BaseDto extends BaseCoreDto implements BaseEntity {
|
||||
creator_id: string;
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const PAGINATION_RESPONSE = 'PAGINATION_RESPONSE';
|
||||
export const GATE_RESPONSE = 'GATE_RESPONSE';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue