Compare commits

...

200 Commits

Author SHA1 Message Date
shancheas afa63667ab Merge branch 'development' into staging 2024-08-14 12:51:27 +07:00
firmanr cefeef8854 Merge pull request 'feat: fixed midtrans' (#73) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #73
2024-08-12 13:01:13 +00:00
Firman Ramdhani f30a23d3c4 feat: fixed midtrans 2024-08-12 20:00:47 +07:00
shancheas dcf0a55dfc fix: remove payment_type_method_id when cancel
continuous-integration/drone/tag Build was killed Details
2024-08-12 19:59:57 +07:00
firmanr 3678353f34 Merge pull request 'feat: fixed midtrans' (#72) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #72
2024-08-12 12:51:36 +00:00
Firman Ramdhani ab903d4554 feat: fixed midtrans 2024-08-12 19:51:17 +07:00
shancheas bd13f50bc0 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development
continuous-integration/drone/tag Build is passing Details
2024-08-12 19:47:21 +07:00
shancheas f4939ffe89 fix: cancel data payment 2024-08-12 19:47:10 +07:00
firmanr 4fe385bca1 Merge pull request 'feat: fixed midtrans' (#71) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #71
2024-08-12 12:41:38 +00:00
Firman Ramdhani be7c74ec72 feat: fixed midtrans 2024-08-12 19:40:54 +07:00
shancheas 88cc9f4bc3 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development
continuous-integration/drone/tag Build is passing Details
2024-08-12 19:34:54 +07:00
shancheas dc595dfb07 fix: mapping transaction cancel 2024-08-12 19:34:34 +07:00
firmanr 211a1e8a3c Merge pull request 'feat: fixed midtrans' (#70) from fix/bug-firman into development
Reviewed-on: #70
2024-08-12 12:30:39 +00:00
Firman Ramdhani 08e018e16b Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into fix/bug-firman 2024-08-12 19:30:20 +07:00
Firman Ramdhani cc62910493 feat: fixed midtrans 2024-08-12 19:29:21 +07:00
firmanr ff44be66e2 Merge pull request 'feat: fixed midtrans' (#69) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #69
2024-08-12 12:27:03 +00:00
Firman Ramdhani b9b1695dc5 feat: fixed midtrans 2024-08-12 19:26:39 +07:00
firmanr 617a08e3f2 Merge pull request 'feat: fixed midtrans' (#68) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #68
2024-08-12 12:14:51 +00:00
Firman Ramdhani aaf6f97e57 feat: fixed midtrans 2024-08-12 19:14:25 +07:00
shancheas 538bd0e58e Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development
continuous-integration/drone/tag Build is passing Details
2024-08-12 18:41:45 +07:00
shancheas ceae06779e fix: booking date undefined 2024-08-12 18:41:28 +07:00
firmanr 5e78669b6c Merge pull request 'feat: fixed midtrans' (#67) from fix/bug-firman into development
continuous-integration/drone/tag Build was killed Details
Reviewed-on: #67
2024-08-12 11:40:21 +00:00
Firman Ramdhani a015353990 feat: fixed midtrans 2024-08-12 18:37:37 +07:00
firmanr 01b4ee2bbd Merge pull request 'feat(SPG-851): fix midtrans' (#66) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #66
2024-08-12 11:02:49 +00:00
Firman Ramdhani c65f0dc9dc feat(SPG-851): fix midtrans 2024-08-12 18:01:59 +07:00
firmanr c98b8da9ba Merge pull request 'feat:' (#65) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #65
2024-08-12 09:18:08 +00:00
Firman Ramdhani bca3682826 feat: 2024-08-12 16:17:36 +07:00
irfan 11aeb44bac Update .drone.yml 2024-08-12 09:04:29 +00:00
irfan 07347c9244 Update .drone.yml 2024-08-12 09:03:32 +00:00
shancheas 29cc6dfae6 fix: add parameter payment total value greater than 0
continuous-integration/drone/tag Build is passing Details
2024-08-12 15:54:21 +07:00
shancheas f9937d84ab fix(SPG-848): exclude counter transaction with null payment type 2024-08-12 15:46:53 +07:00
shancheas a14119719c ci: change mattermost token 2024-08-12 13:56:45 +07:00
shancheas ae75578b65 ci: remove timeout
continuous-integration/drone/tag Build is passing Details
2024-08-12 13:45:56 +07:00
shancheas 93fa32df80 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development
continuous-integration/drone/tag Build is failing Details
2024-08-12 13:42:03 +07:00
shancheas f7d7fba267 ci: change pipeline formula 2024-08-12 13:41:43 +07:00
firmanr ebcc5515e5 Merge pull request 'fix/bug-firman' (#64) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #64
2024-08-12 03:52:05 +00:00
Firman Ramdhani b597a2f184 feat(SPG-265): report reconciliation 2024-08-12 10:50:02 +07:00
Firman Ramdhani 7a6c784612 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into fix/bug-firman 2024-08-09 17:38:47 +07:00
Firman Ramdhani d6fc817cef feat: remove unused report 2024-08-09 17:38:35 +07:00
firmanr 89aa2a68b4 Merge pull request 'fix/bug-firman' (#63) from fix/bug-firman into development
Reviewed-on: #63
2024-08-09 10:23:54 +00:00
Firman Ramdhani 64d812bef1 feat(SPG-267): report cash withdrawals 2024-08-09 17:21:12 +07:00
Firman Ramdhani 3b8310581b feat(SPG-266): report cashier log 2024-08-09 16:10:57 +07:00
Firman Ramdhani 21b9549b52 feat(SPG-264): report refund 2024-08-09 16:06:17 +07:00
Firman Ramdhani 76b518614f feat(SPG-263): report pemesanan 2024-08-09 15:52:53 +07:00
Firman Ramdhani 841f8889ec Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into fix/bug-firman 2024-08-09 15:43:25 +07:00
Firman Ramdhani bc8476a56e feat(SPG-268): report pemberian diskon 2024-08-09 15:43:13 +07:00
shancheas fe6572a770 fix: only check transaction more than 1 day
continuous-integration/drone/tag Build is passing Details
2024-08-09 15:23:20 +07:00
shancheas acf8861823 feat: add sync midtrans transaction 2024-08-09 15:18:43 +07:00
Firman Ramdhani a319b64abe feat(SPG-259): report pendapatan per item 2024-08-09 15:14:37 +07:00
firmanr 3938504fd3 Merge pull request 'feat: fix access control download' (#62) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #62
2024-08-09 04:27:43 +00:00
Firman Ramdhani 3b07c8de99 feat: fix access control download 2024-08-09 11:27:00 +07:00
firmanr fc67c222f8 Merge pull request 'feat(SPG-258): create report income and fix calculate hpp on transaction' (#61) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #61
2024-08-09 04:18:33 +00:00
Firman Ramdhani f0c1410532 feat(SPG-258): create report income and fix calculate hpp on transaction 2024-08-09 11:16:56 +07:00
shancheas c5fdca615f fix(SPG-816): add payment date and payment method
continuous-integration/drone/tag Build is passing Details
2024-08-08 18:24:59 +07:00
shancheas e00a6aae31 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development
continuous-integration/drone/tag Build is passing Details
2024-08-08 18:10:32 +07:00
shancheas f5425ccfb1 feat: add change status midtrans 2024-08-08 18:10:18 +07:00
firmanr 995f4b963c Merge pull request 'fix/bug-firman' (#60) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #60
2024-08-08 10:29:37 +00:00
Firman Ramdhani 13895394b3 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into fix/bug-firman 2024-08-08 17:28:42 +07:00
Firman Ramdhani b36e35ea00 feat(SPG-823): add item access control 2024-08-08 17:28:17 +07:00
shancheas e9c819987e fix: add payment_midtrans_url to transactions
continuous-integration/drone/tag Build was killed Details
2024-08-08 17:27:51 +07:00
shancheas a61f8b853d fix(SPG-817): invoice expired not send email
continuous-integration/drone/tag Build is passing Details
2024-08-08 17:07:03 +07:00
shancheas efe5661a57 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development
continuous-integration/drone/tag Build is passing Details
2024-08-08 15:47:53 +07:00
shancheas 30d3d91bba fix(SPG-817): send email when status expired 2024-08-08 15:47:39 +07:00
firmanr 1fa4d315be Merge pull request 'fix/bug-firman' (#59) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #59
2024-08-08 08:42:34 +00:00
shancheas bcdf656d5d fix: midtrans error total != qty * price 2024-08-08 15:34:34 +07:00
Firman Ramdhani 63152ec90e feat(SPG-833): add key refund_type on get index 2024-08-08 15:24:35 +07:00
Firman Ramdhani 990d73bdb1 feat(SPG-837): validate delete and in active when data session type has relation 2024-08-08 15:07:38 +07:00
Firman Ramdhani 1dae9ec356 feat(SPG-780): sync data session type 2024-08-08 14:51:38 +07:00
Firman Ramdhani 8de744bc58 feat(SPG-782): update base price at item rate when price on item rate not edited before 2024-08-08 13:19:03 +07:00
Firman Ramdhani f8618ec0cd Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into fix/bug-firman 2024-08-08 11:28:54 +07:00
shancheas 1e8a07ec55 fix: change pos transaction event from update to change status 2024-08-08 11:27:20 +07:00
Firman Ramdhani 810a1c5bf5 feat: add planning comment for handle change status from multiple tabs 2024-08-07 22:18:19 +07:00
firmanr 1d8a3d7ff9 Merge pull request 'feat(SPG-818): fix condition transaction status change handler' (#58) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #58
2024-08-07 11:47:44 +00:00
Firman Ramdhani b57da1d21a feat(SPG-818): fix condition transaction status change handler 2024-08-07 18:47:08 +07:00
shancheas 8dd36042eb fix(SPG-804): cancel transaction to pending booking
continuous-integration/drone/tag Build is passing Details
2024-08-07 18:04:53 +07:00
firmanr 72e47c2486 Merge pull request 'feat(SPG-737): fix filter reconciliation' (#57) from fix/bug-firman into development
Reviewed-on: #57
2024-08-07 10:46:32 +00:00
Firman Ramdhani cfb0d3c60f feat(SPG-792) : fix sync transaction data 2024-08-07 17:46:14 +07:00
Firman Ramdhani 0c3abc6a8f feat: add get data on couch service 2024-08-07 16:41:13 +07:00
Firman Ramdhani 1fcbe57018 feat: remove console 2024-08-07 16:33:20 +07:00
Firman Ramdhani ba6ee4a408 feat(SPG-737): fix filter reconciliation 2024-08-07 14:47:19 +07:00
shancheas e4a631b929 fix: move google credential to folder
continuous-integration/drone/tag Build is passing Details
2024-08-07 12:30:55 +07:00
shancheas dbdc7a0203 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development
continuous-integration/drone/tag Build is passing Details
2024-08-07 11:48:55 +07:00
shancheas fcaf4a07a1 log: add APM to handler 2024-08-07 11:48:39 +07:00
firmanr 9efa56b2bc Merge pull request 'feat(SPG-800): add validation delete on data booking transaction' (#56) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #56
2024-08-07 04:32:44 +00:00
Firman Ramdhani 0658b503bd feat(SPG-656): allow input number on formula 2024-08-07 11:05:37 +07:00
Firman Ramdhani 9802d983bb feat(SPG-800): add validation delete on data booking transaction 2024-08-07 10:31:19 +07:00
firmanr 583b754315 Merge pull request 'Update env/env.production' (#55) from fix/bug-firman into development
Reviewed-on: #55
2024-08-06 10:40:09 +00:00
Firman Ramdhani 4ef139b2f5 Update env/env.production 2024-08-06 17:39:53 +07:00
irfan b137e1abce Update env/env.development
continuous-integration/drone/tag Build is passing Details
2024-08-06 10:38:18 +00:00
shancheas e8baaa12d2 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development 2024-08-06 16:06:43 +07:00
shancheas 6ca74ad6f7 fix: lint 2024-08-06 16:06:30 +07:00
firmanr 955f00cc9d Merge pull request 'feat(SPG-793): fix time at report log' (#54) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #54
2024-08-06 09:05:27 +00:00
Firman Ramdhani b48a469720 feat(SPG-772): fix key settlement date on booking module 2024-08-06 16:04:20 +07:00
shancheas 61d8f56385 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development
continuous-integration/drone/tag Build was killed Details
2024-08-06 16:04:13 +07:00
shancheas 2a42d85814 chore: add vscode debug configuration 2024-08-06 16:03:55 +07:00
shancheas bacdb1773b fix: item rates not generated when create period season 2024-08-06 16:03:42 +07:00
Firman Ramdhani 655157239d feat(SPG-793): fix time at report log 2024-08-06 13:23:21 +07:00
firmanr 33014394d1 Merge pull request 'feat(SPG-773): set item name to unique' (#53) from fix/bug-firman into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #53
2024-08-06 05:48:50 +00:00
Firman Ramdhani aaee458df6 feat(SPG-773): set item name to unique 2024-08-06 12:47:09 +07:00
firmanr ffcbf65d9d Merge pull request 'feat/report' (#52) from feat/report into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #52
2024-08-06 05:35:06 +00:00
Firman Ramdhani 2f19db8dd7 feat(SPG-777): add validation delete for tenant item bundling 2024-08-06 11:40:18 +07:00
Firman Ramdhani e7664be8a8 feat(SPG-777): add validation delete for tenant item bundling 2024-08-06 11:39:40 +07:00
Firman Ramdhani a2c1af2a65 merge 2024-08-06 11:38:17 +07:00
Firman Ramdhani 26db7d2745 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into feat/report 2024-08-06 11:37:59 +07:00
Firman Ramdhani e922db827d feat(SPG-777): add validation delete for tenant item bundling 2024-08-06 11:33:51 +07:00
shancheas 5dde995ab8 fix: fail to cancel transaction
continuous-integration/drone/tag Build is passing Details
2024-08-06 10:37:56 +07:00
shancheas 79d1c564ff log: add APM log to couch handler
continuous-integration/drone/tag Build is passing Details
2024-08-06 06:16:10 +07:00
shancheas e7abc7db13 fix: move mail template to assets folder 2024-08-06 06:13:13 +07:00
shancheas 81a463e761 chore: update file location
continuous-integration/drone/tag Build is passing Details
2024-08-05 15:37:27 +07:00
shancheas c741a55577 fix: move image from file to base64 string
continuous-integration/drone/tag Build is passing Details
2024-08-05 15:18:24 +07:00
shancheas 81dad9a69c fix: change logo image location
continuous-integration/drone/tag Build is passing Details
2024-08-05 15:00:13 +07:00
shancheas 8999ed1bf0 fix: error image and typo
continuous-integration/drone/tag Build is passing Details
2024-08-05 14:43:42 +07:00
aswin ebfcbb85fe Merge pull request 'feat/pdf-generator' (#51) from feat/pdf-generator into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #51
2024-08-03 19:52:58 +00:00
Aswin Ashar Abdullah feee9a3439 fix(SPG-494) PDF Generator 2024-08-04 02:51:11 +07:00
aswin b05e6fab5a Merge pull request 'development' (#50) from development into feat/pdf-generator
Reviewed-on: #50
2024-08-02 12:01:09 +00:00
aswin 37350e454f Merge pull request 'fix/data' (#49) from fix/data into development
Reviewed-on: #49
2024-08-02 12:00:23 +00:00
Aswin Ashar Abdullah b08325a53c fix(email) checkpoint email service 2024-08-02 18:59:31 +07:00
Aswin Ashar Abdullah a1f7108bc5 feat(SPG-233) Integration Google Calendar API 2024-08-02 18:36:42 +07:00
Aswin Ashar Abdullah 2e4d5df17a fix(SPG-762) BE - integrasi dengan API Midtrans - untuk update status pembayaran 2024-08-02 18:11:36 +07:00
Aswin Ashar Abdullah 4fcd852d8d fix(SPG-769) save edit data terkena validasi status transaksi settled 2024-08-02 17:59:30 +07:00
Aswin Ashar Abdullah 0f05656ca2 fix(booking) perbaikan column payment type counter 2024-08-02 17:54:12 +07:00
Aswin Ashar Abdullah 0bec3c6590 fix(SPG-750) Ketika sales di PoS di cancel, pada rekondiliasi - recap, valuenya tidak berkurang 2024-08-02 17:53:29 +07:00
Aswin Ashar Abdullah 8eee99fce5 fix(SPG-745) Data booking status active dan settled (tipe pembayaran selain counter) belum masuk PoS 2024-08-02 17:49:11 +07:00
Aswin Ashar Abdullah 239e2d778a feat(SPG-760) Validasi Bundling dan Item active inactive 2024-08-02 17:12:12 +07:00
Aswin Ashar Abdullah cc01b23e2a fix(SPG-754) Di hak akses PoS, penjualan seharusnya tidak ada check box delete 2024-08-02 16:56:34 +07:00
Aswin Ashar Abdullah 0b502188af fix(SPG-687) Booking status pending seharusnya bisa di cancel 2024-08-02 16:08:26 +07:00
Aswin Ashar Abdullah e38fbc65bd fix(SPG-759) Ketika ada item baru dibuat ( kondisi sudah ada data item rate untuk product lain ) 2024-08-02 16:00:05 +07:00
Aswin Ashar Abdullah 0e47b99ca7 fix(SPG-756) Untuk value field sales margin (%) seharusnya bisa mempunyai value dibelakang koma 2024-08-02 15:28:56 +07:00
Aswin Ashar Abdullah e401a1bf4c fix(SPG-742) Filter status, sumber, tipe pembayaran, pembayaran via, bank, tgl konfirmasi belum jalan 2024-08-02 15:16:43 +07:00
Aswin Ashar Abdullah 1636f6b930 fix(SPG-743) Refund dengan code invoice yang sama terkena validasi 2024-08-02 14:38:36 +07:00
aswin d1892e7aa5 Merge pull request 'development' (#48) from development into feat/pdf-generator
Reviewed-on: #48
2024-08-02 06:32:20 +00:00
Firman Ramdhani d14d9101ae feat: add report CashierLogReport and CashWithdrawalsReport
continuous-integration/drone/tag Build is passing Details
2024-08-01 18:09:40 +07:00
Firman Ramdhani 746e24feb6 feat: add migration for table pos log
continuous-integration/drone/tag Build is passing Details
2024-08-01 17:48:21 +07:00
Firman Ramdhani 39d7eb28b9 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development 2024-08-01 17:45:55 +07:00
Firman Ramdhani 9d0d76120e feat: update model pos log 2024-08-01 17:45:33 +07:00
aswin 971407612c Merge pull request 'fix/data' (#47) from fix/data into development
Reviewed-on: #47
2024-08-01 10:37:35 +00:00
Aswin Ashar Abdullah 03ec533e86 fix(SPG-736) Cancel rekon statu Pending terkena validasi data pemesanan harus settled 2024-08-01 17:33:36 +07:00
Aswin Ashar Abdullah d413bd771b fix(SPG-735) Ketika recap di click berkali kali, transaksi menggunakan tipe cash jadi duplicate 2024-08-01 17:25:12 +07:00
Aswin Ashar Abdullah 11171b2859 fix(SPG-729) pemesanan data seharusnya berstatus proses refund ketika data refund berstatus draft 2024-08-01 17:01:39 +07:00
Firman Ramdhani 1e9cc9da4f feat: add default condition for type on booking report 2024-08-01 15:13:00 +07:00
Firman Ramdhani 46307774e1 Merge branch 'development' of ssh://git.eigen.co.id:2222/eigen/pos-be into development 2024-08-01 14:43:43 +07:00
Firman Ramdhani 62eccf29a5 feat: rename header title report 2024-08-01 14:43:37 +07:00
Aswin Ashar Abdullah 383fdce9f7 feat(pdf) checkpoint pdf 2024-08-01 14:36:20 +07:00
Aswin Ashar Abdullah 1ae7f4e097 fix(booking) perbaikan event deleted booking 2024-08-01 14:27:57 +07:00
aswin 33f955c209 Merge pull request 'fix/data' (#46) from fix/data into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #46
2024-08-01 07:01:41 +00:00
Aswin Ashar Abdullah d4bd3d746c fix(filter) perbaikan filter season period 2024-08-01 14:00:14 +07:00
Aswin Ashar Abdullah 45f11003f2 fix(privilege) perbaikan privilege constants 2024-08-01 13:33:02 +07:00
Aswin Ashar Abdullah 2188d63943 fix(SPG-726) Update image pada item status active, data image tidak ter sync ke PoS 2024-07-31 19:05:08 +07:00
Aswin Ashar Abdullah 3af56fa5d5 fix(SPG-652) Item Tipe Bundling - validasi 2024-07-31 18:51:31 +07:00
aswin 77d8e7ae1e Merge pull request 'fix/data' (#45) from fix/data into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #45
2024-07-31 11:27:39 +00:00
Aswin Ashar Abdullah c49bb379bd fix(item price) perbaikan price item 2024-07-31 18:25:28 +07:00
Aswin Ashar Abdullah 0648eeffee fix(SPG-711) Internal server error sering sekali ketika confirm status draft ke status pending 2024-07-31 18:24:47 +07:00
Aswin Ashar Abdullah e8deba2882 fix(SPG-709) Data summary transaksi PoS tidak muncul di halaman rekonsiliasi setelah click button recap 2024-07-31 17:58:33 +07:00
Aswin Ashar Abdullah 19386c336e fix(SPG-710) ganti warning cancel jadi bahasa indonesia 2024-07-31 17:58:01 +07:00
Aswin Ashar Abdullah 58cf3f7ab0 fix(SPG-718) Validasi perubahan tipe wahana seharusnya tidak memvalidasi perubahan namanya 2024-07-31 16:29:04 +07:00
Aswin Ashar Abdullah d7c4b27749 feat(SPG-719) Season Period - API Index - tambahkan filter berdasarkan season type 2024-07-31 16:19:30 +07:00
Aswin Ashar Abdullah 0ac5754170 fix(SPG-703) Sync account superadmin ke Pouch Couch supaya bisa login di PoS 2024-07-31 16:11:59 +07:00
Aswin Ashar Abdullah b7557a5f19 fix(SPG-663) Pos Privileges - pemesanan seharusnya tidak ada create 2024-07-31 15:53:41 +07:00
aswin 1590080468 Merge pull request 'fix(transaction) perbaikan data' (#44) from fix/data into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #44
2024-07-30 12:26:06 +00:00
Aswin Ashar Abdullah 61c512f7f4 fix(transaction) perbaikan data 2024-07-30 19:25:22 +07:00
aswin 30ffdca550 Merge pull request 'fix/data' (#43) from fix/data into development
continuous-integration/drone/tag Build is failing Details
Reviewed-on: #43
2024-07-30 11:33:11 +00:00
Aswin Ashar Abdullah c154af6cb7 fix(SPG-708) Muncul error saat meng cancel status pending - warninnya hanya bisa meng cancel status pending juga 2024-07-30 18:26:50 +07:00
Aswin Ashar Abdullah 197b3478ae fix(SPG_693) di hak akses pos pada bagian penjualan seharusnya ada checkbox pada kolom cancel dan ada hak akses reprint receipt 2024-07-30 18:22:23 +07:00
Aswin Ashar Abdullah c6be42299e fix(SPG-697) Pada index booking seharusnya transaksi pos tidak muncul, dan sesuaikan alur booking dengan pembayaran counter 2024-07-30 17:12:55 +07:00
Firman Ramdhani 9bcc72a69e feat: fix set false bookmark report
continuous-integration/drone/tag Build is passing Details
2024-07-30 16:59:11 +07:00
Aswin Ashar Abdullah dc6476a66f fix(SPG-687) Booking status pending seharusnya bisa di cancel, tapi ketika di cancel muncul error message, hanya status active yang bisa di cancel 2024-07-30 14:44:12 +07:00
Aswin Ashar Abdullah 4a9ca5eb5b fix(SPG-688) Ketika tgl booking di rubah web admin, tanggal booking pada PoS tidak ikut terupdate 2024-07-30 14:36:49 +07:00
aswin e1004b3843 Merge pull request 'fix/data' (#42) from fix/data into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #42
2024-07-30 06:56:07 +00:00
Aswin Ashar Abdullah 6632222c4b fix(SPG-677) Pada Index tgl invoice belum ada datanya, invoice data = tgl ketika booking di confirm ke status pending 2024-07-30 13:54:00 +07:00
Aswin Ashar Abdullah 945edbf76d fix(SPG-659) Muncul error get data saat confirm transaksi status draft ke pending 2024-07-30 13:50:23 +07:00
Aswin Ashar Abdullah 190f42d598 fix(SPG-645) Button generate price belum berfungsi 2024-07-30 13:46:57 +07:00
Aswin Ashar Abdullah d9bbe1290d fix(SPG-675) Error saat confirm status pending ( hanya bisa refund status settled ) padahal status sudah berubah ke proces refund 2024-07-30 13:08:50 +07:00
Aswin Ashar Abdullah 3e85d40885 feat(SPG-651) BE Reason Refund Request 2024-07-30 13:00:49 +07:00
Aswin Ashar Abdullah 7e38a67e80 fix(SPG-667) Kategori item status inactive seharusnya tidak terpanggil 2024-07-30 12:44:57 +07:00
Aswin Ashar Abdullah b91d9f7da8 fix(SPG-671) Item Bundling - validasi / warning jika item dalam bundling diinactivekan sedangkan masih berelasi, bundling harus di inactivekan dulu atau didelete 2024-07-30 12:24:31 +07:00
irfan bf8987242b Merge pull request 'fix: change query runner to repository from base manager' (#41) from fix/base-query into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #41
2024-07-29 09:47:00 +00:00
shancheas 57f2700c94 fix: change query runner to repository from base manager 2024-07-29 16:44:23 +07:00
Aswin Ashar Abdullah b3f0752ca0 fix(SPG-662) Search title banner belum jalan 2024-07-29 14:53:22 +07:00
Aswin Ashar Abdullah 33438f37ec fix(SPG-669) Validasi perubahan tipe wahana jika sudah mempunyai item yang berelasi 2024-07-29 14:48:05 +07:00
Aswin Ashar Abdullah 593955574a fix(SPG-664) admin Privilles - rekonsiliasi seharusnya tidak ada create 2024-07-29 14:37:46 +07:00
Aswin Ashar Abdullah 4ac711add5 fix(SPG-663) Pos Privilleges - pemesanan seharusnya tidak ada create 2024-07-29 14:37:12 +07:00
aswin c7fa402663 Merge pull request 'fix/data' (#40) from fix/data into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #40
2024-07-26 10:20:47 +00:00
Aswin Ashar Abdullah 6096771597 fix(SPG-658) Validasi - tidak bisa confirm dari status pending ke waiting, ketika data pembayaran booking belum diisi ( untuk pembayaran bank transfer dan QRIS saja) 2024-07-26 17:14:04 +07:00
Aswin Ashar Abdullah 76fa380f1e fix(SPG-657) Kode Pembayaran - ketika booking status waiting di confirm di rekonsiliasi, kode pembayaran tidak muncul 2024-07-26 16:49:52 +07:00
Aswin Ashar Abdullah 1f4bf80908 fix(SPG-661) Filter rekonsiliasi exclude status draft 2024-07-26 16:28:25 +07:00
Aswin Ashar Abdullah 66c481c9b0 fix(SPG-645) Button generate price belum berfungsi 2024-07-26 16:20:22 +07:00
Aswin Ashar Abdullah 9cd50ad817 fix(SPG-655) Item Rate saat filter harga jadi mengambil base price (kemungkinan ada salah ambil priority 2024-07-26 15:43:34 +07:00
Aswin Ashar Abdullah 82105e8214 fix(rate) perbaikan update rate 2024-07-26 15:05:28 +07:00
Aswin Ashar Abdullah e68524380a fix(SPG-652) Item Tipe Bundling - Item yang sudah berrelasi atau dipakai pada bundling tidak dapat di delete atau di inactivekan sampai di lepas dari bundling nya 2024-07-26 15:03:37 +07:00
aswin d333ba03a7 Merge pull request 'fix(season) update fix priority' (#39) from fix/data into development
Reviewed-on: #39
2024-07-26 07:04:31 +00:00
Aswin Ashar Abdullah a63eaf0110 fix(season) update fix priority 2024-07-26 14:03:38 +07:00
Aswin Ashar Abdullah e32a7c2eaf fix(base) delete query transaction
continuous-integration/drone/tag Build is passing Details
2024-07-26 11:41:18 +07:00
aswin 07f379a6b0 Merge pull request 'fix/data' (#38) from fix/data into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #38
2024-07-26 03:06:59 +00:00
Aswin Ashar Abdullah 39896fed90 fix(env) update env value 2024-07-26 10:06:11 +07:00
Aswin Ashar Abdullah a671404947 feat(SPG-554) BE - Integrasi Booking dengan Google Calendar 2024-07-25 16:03:43 +07:00
aswin 00d5eba7ab Merge pull request 'fix/data' (#37) from fix/data into development
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #37
2024-07-23 12:58:11 +00:00
Aswin Ashar Abdullah d0163ae003 feat(SPG-125) Logging 2024-07-23 19:54:00 +07:00
Aswin Ashar Abdullah aa550f9d38 feat(SPG-612) Activity Log PoS 2024-07-23 19:24:40 +07:00
Aswin Ashar Abdullah 2fc80c7cfc fix(SPG-640) Sync harga ketika ditambahkan melalui item rate (case item sudah dibuat) 2024-07-23 18:25:57 +07:00
Aswin Ashar Abdullah 20848aab3c fix(SPG-644) Sync ketika privileges di update 2024-07-23 18:25:14 +07:00
231 changed files with 7590 additions and 856 deletions

View File

@ -1,38 +1,35 @@
kind: pipeline
type: docker
name: build
name: server
steps:
- name: build-dev
image: plugins/docker
- name: build
image: appleboy/drone-ssh
settings:
registry: registry.eigen.co.id
repo: registry.eigen.co.id/eigen/${DRONE_REPO_NAME}
build_args:
- env_target=env.development
tags: latest
custom_dns: 172.10.10.16
host:
- 172.10.10.10
username: eigen
key:
from_secret: DEVOPS_SSH_PRIVATE_OPEN
port: 22
script:
- cd /home/eigen/PROJECT/POS/POS.DEV/BE
- sh build.sh
- name: send-message
image: plugins/webhook
settings:
urls: https://mattermost.eigen.co.id/api/v4/posts
content_type: application/json
headers:
- Authorization=Bearer 5zubexudb38uuradfa36qy98ca
template: |
{
"channel_id": "s1ekqde1c3du5p35g6budnuotc",
"message": "Build {{repo.name}} sudah selesai"
}
trigger:
ref:
- refs/tags/devel_*
- refs/tags/*-alpha.*
event:
exclude:
- promote
---
kind: pipeline
type: docker
name: deployment
steps:
- name: deployment
image: alpine
failure: ignore
commands:
- apk add --no-cache curl
- curl -X POST https://manager.sky.eigen.co.id/api/webhooks/806de7e2-1d3e-4889-b472-a59af0a5eb33
trigger:
ref:
- refs/tags/devel_*
event:
exclude:
- promote
depends_on:
- build

38
.drone.yml.old Normal file
View File

@ -0,0 +1,38 @@
kind: pipeline
type: docker
name: build
steps:
- name: build-dev
image: plugins/docker
settings:
registry: registry.eigen.co.id
repo: registry.eigen.co.id/eigen/${DRONE_REPO_NAME}
build_args:
- env_target=env.development
tags: latest
custom_dns: 172.10.10.16
trigger:
ref:
- refs/tags/devel_*
event:
exclude:
- promote
---
kind: pipeline
type: docker
name: deployment
steps:
- name: deployment
image: alpine
failure: ignore
commands:
- apk add --no-cache curl
- curl -X POST https://manager.sky.eigen.co.id/api/webhooks/806de7e2-1d3e-4889-b472-a59af0a5eb33
trigger:
ref:
- refs/tags/devel_*
event:
exclude:
- promote
depends_on:
- build

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

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Nest Framework",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
"autoAttachChildProcesses": true,
"restart": true,
"sourceMaps": true,
"stopOnEntry": false,
"console": "integratedTerminal"
}
]
}

View File

@ -9,5 +9,6 @@ FROM node:18.17-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/assets ./assets
COPY --from=builder /app/package.json ./package.json
CMD ["node", "--max-old-space-size=8192","--max-http-header-size", "512000", "-r", "dotenv/config", "dist/main"]

View File

@ -4,7 +4,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email Ibunda</title>
<title>Email Confirmation</title>
<style>
/* -------------------------------------
GLOBAL RESETS
@ -364,45 +364,36 @@
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Halo {{customer_name}}</p>
<p>Pemesanan tiket telah berhasil dengan nomor invoice {{invoice_code}}, Silahkan lakukan pembayaran</p>
<p class="mb0"><b>Dear,</b></p>
<p class="mb0">{{customer_name}}</p>
<p>{{customer_phone}}</p>
<p class="mb0">Great News! We've successfully updated your booking date as per your request.</p>
<p>We're excited to accommodate your new plans and ensure evertyhing goes smoothly.</p>
<p class="mb0">Here are your updated booking details</p>
<b class="mb0">Original Booking Date: {{booking_date_before}}</b>
<b>New Booking Date: {{booking_date}}</b>
<p class="mb0">For yout convenience, we've attached a new confirmation receipt reflecting these changes.</p>
<p><b>Please be sure to bring this updated receipt with you on the new date</b></p>
<p class="mb0">To keep the good times rolling, our friendly support team is just a call away at</p>
<b>{{phone_cs}}</b>
<br>
<p>
<b>PEMBAYARAN DAPAT MELALUI</b>
<ul>
{{#each payment_methods}}
<li>
<p>
<b>{{issuer_name}}</b><br>
<span>Name: <b>{{account_name}}</b></span><br>
<span>Number: <b>{{account_number}}</b></span>
</p>
</li>
{{/each}}
</ul>
</p>
<p>Thank you and we can't wait to see you and make sure you have an amazing time!</p>
<br><br>
<b>Best Regrads,</b><br>
<b>WEplayground</b>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block powered-by">
Powered by Skyworld
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>

View File

@ -0,0 +1,412 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email Confirmation</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
width: 100%;
}
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn>tbody>tr>td {
padding-bottom: 15px;
}
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
ol {
padding: 0 0 0 1em;
}
ol li {
margin: 1em 0;
}
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p class="mb0"><b>Dear,</b></p>
<p class="mb0">{{customer_name}}</p>
<p>{{customer_phone}}</p>
<p>Thank you fot choosing us! We're absolutelty thrilled and can't wait to embark on this exciting day with you. See you soon for fun times ahead</p>
<p class="mb0">Here's a quick recap of your invoice:</p>
<p class="mb0">Booking Date: {{booking_date}}</p>
<p>Total Invoice: {{payment_total}}</p>
<p class="mb0">Just a friendly reminder that your invoice will expire on {{expire_date}}</p>
<p>To keep things running smoothly, please ensure your payment is completed before this data</p>
<p>
For your convenience, here is a list of our account details:
<ul>
{{#each payment_methods}}
<li>
<p>{{issuer_name}} {{account_number}} a/n >{{account_name}}</p>
</li>
{{/each}}
</ul>
</p>
<p class="mb0">Once you've made the payment, please kindly email or send the proof of payment so we can proceed with your booking promptly</p>
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
<b>{{phone_cs}}</b>
<br><br>
<b>Best Regrads,</b><br>
<b>WEplayground</b>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -4,7 +4,7 @@
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email Ibunda</title>
<title>Email Confirmation</title>
<style>
/* -------------------------------------
GLOBAL RESETS
@ -364,36 +364,32 @@
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p>Halo {{customer_name}}</p>
<p>Pemesanan tiket telah berhasil dengan nomor invoice {{invoice_code}}, Silahkan lakukan pembayaran dengan klik button dibawah ini</p>
</td>
</tr>
<tr>
<td align="center"
style="font-family: 'Lato', sans-serif; font-size:22px; color:#e5eaf5; line-height:24px; font-weight: 600;">
<a href="{{payment_midtrans_url}}">Lanjutkan Pembayaran</a>
<p class="mb0"><b>Dear,</b></p>
<p class="mb0">{{customer_name}}</p>
<p>{{customer_phone}}</p>
<p>We hope this message finds you well!</p>
<p class="mb0">Uh-oh! it looks like your invoice, dated {{invoice_date}}, has officially expired as of {{expired_date}}</p>
<p>But no worries, we can fix this together!</p>
<p class="mb0">To keep the good times rolling, our friendly support team is just a call away at</p>
<b>{{phone_cs}}</b>
<p class="mb0">Here are the details of the expired invoice:</p>
<p class="mb0">Booking Date: {{booking_date}}</p>
<p>Total Invoice: {{total_payment}}</p>
<br><br>
<b>Best Regrads,</b><br>
<b>WEplayground</b>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block powered-by">
Powered by Skyworld
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>

View File

@ -0,0 +1,419 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email Confirmation</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
width: 100%;
}
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn>tbody>tr>td {
padding-bottom: 15px;
}
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
ol {
padding: 0 0 0 1em;
}
ol li {
margin: 1em 0;
}
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p class="mb0"><b>Dear,</b></p>
<p class="mb0">{{customer_name}}</p>
<p>{{customer_phone}}</p>
<p>Thank you fot choosing us! We're absolutelty thrilled and can't wait to embark on this exciting day with you. See you soon for fun times ahead</p>
<p class="mb0">Here's a quick recap of your invoice:</p>
<p class="mb0">Booking Date: {{booking_date}}</p>
<p>Total Invoice: {{payment_total}}</p>
<p class="mb0">Just a friendly reminder that your invoice will expire on {{expire_date}}</p>
<p>To keep things running smoothly, please ensure your payment is completed before this data</p>
<p class="mb0">For your convenience, here is a list of our account details:</p>
<a href="{{payment_midtrans_url}}">{{payment_midtrans_url}}</a>
<br><br>
<p class="mb0">Once you've made the payment, please kindly email or send the proof of payment so we can proceed with your booking promptly</p>
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
<b>{{phone_cs}}</b>
<br><br>
<b>Best Regrads,</b><br>
<b>WEplayground</b>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<!-- <div class="footer">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block powered-by">
Powered by Skyworld
</td>
</tr>
</table>
</div> -->
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,412 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email Confirmation</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
width: 100%;
}
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn>tbody>tr>td {
padding-bottom: 15px;
}
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
ol {
padding: 0 0 0 1em;
}
ol li {
margin: 1em 0;
}
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p class="mb0"><b>Dear,</b></p>
<p class="mb0">{{customer_name}}</p>
<p>{{customer_phone}}</p>
<p class="mb0">We are excited to inform you that your payment has been successfully received!</p>
<p class="mb0">Attached to this email, you will find your confirmatin receipt</p>
<p class="mb0">Please keep this safe as you will need to show it at the entrance upon your arrival</p>
<p>It's your golden ticket to all the fun and excitement awaiting you!</p>
<br>
<p class="mb0">Here's a quick recap:</p>
<p class="mb0">Booking Date: {{booking_date}}</p>
<p class="mb0">Invoice Code: {{invoice_code}}</p>
<p class="mb0">Payment Date: {{payment_date}}</p>
<p class="mb0">Payment Code: {{payment_code}}</p>
<p class="mb0">Payment Via: {{payment_via}}</p>
<p class="mb0">Account No: {{account_no}}</p>
<p>On Behalf Of: {{account_name}}</p>
<br>
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
<b>{{phone_cs}}</b>
<br>
<p class="mb0">Font forget to bring a smile and your confirmation receipt (attached) for a smooth entry</p>
<p>We can't wait to see you and ensure you have an amazing time with us!</p>
<br><br>
<b>Best Regrads,</b><br>
<b>WEplayground</b>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,415 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email Confirmation</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
width: 100%;
}
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn>tbody>tr>td {
padding-bottom: 15px;
}
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
ol {
padding: 0 0 0 1em;
}
ol li {
margin: 1em 0;
}
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p class="mb0"><b>Dear,</b></p>
<p class="mb0">{{customer_name}}</p>
<p>{{customer_phone}}</p>
<p class="mb-0">Good News!</p>
<p>We've successfully processed your refund for:</p>
<p class="mb0">Here are the details of your refund:</p>
<p class="mb0">Transaction Date: <b>{{booking_date}}</b></p>
<p class="mb0">Transaction Code: <b>{{invoice_code}}</b></p>
<p class="mb0">Total Refund: <b>{{refund.refund_total}}</b></p>
<p>{{{refund_items}}}</p>
<p class="mb0">Transaction Number: <b>{{invoice_code}}</b></p>
<p class="mb0">Refund Processed Date: <b>{{refund.refund_date}}</b></p>
<p class="mb0">Bank Account: <b>{{refund.bank_name}}</b></p>
<p class="mb0">Account Number: <b>{{refund.bank_account_number}}</b></p>
<p>Account Name: <b>{{refund.bank_account_name}}</b></p>
<p class="mb0">We hope this helps make things right, and we're here to assist if you need anything else</p>
<p>You should see the refund in your account within 3 business days</p>
<p class="mb0">Thank you for your patience and understanding</p>
<p>If you have any questions or need further assistance, don't hesitate to reach out us at</p>
<b>{{phone_cs}}</b>
<br>
<p class="mb0">Thank you and we can't wait to see you and make sure you have an amazing time!</p>
<br><br>
<b>Best Regrads,</b><br>
<b>WEplayground</b>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,409 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email Confirmation</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
width: 100%;
}
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink
down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
Margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
Margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%;
}
.btn>tbody>tr>td {
padding-bottom: 15px;
}
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
ol {
padding: 0 0 0 1em;
}
ol li {
margin: 1em 0;
}
</style>
</head>
<body class="">
<table border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<table class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<p class="mb0"><b>Dear,</b></p>
<p class="mb0">{{customer_name}}</p>
<p>{{customer_phone}}</p>
<p class="mb0">We're trully sorry for any inconvenience that led to this request</p>
<p>We've received your refund request for :</p>
<p class="mb0">Transaction Date: <b>{{booking_date}}</b></p>
<p class="mb0">Transaction Code: <b>{{invoice_code}}</b></p>
<p class="mb0">Refund Code: <b>{{refund.code}}</b></p>
<p class="mb0">Total Refund: <b>{{refund.refund_total}}</b></p>
<p>{{{refund_items}}}</p>
<p class="mb0">Your satisfaction is important to us, and we're commited to resolving this as quickly as possible</p>
<p class="mb0">Our team is already on it and will process your refund request promptly</p>
<p>We'll keep you updated and notify you once the refund has been processed</p>
<p class="mb0">If you have any questions or need assistance, feel free to reach out to our support team at</p>
<b>{{phone_cs}}</b>
<br>
<p class="mb0">Thank you for your patience and understanding</p>
<p>We appriciate your feedback and are here to make things right</p>
<br><br>
<b>Best Regrads,</b><br>
<b>WEplayground</b>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/image/logo.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "weplayground-app",
"private_key_id": "e3ed1a4430140ac589c6e9e7ce125d16d8f7304a",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCmEl90K7ojdx1\nnJv5BKq3THI+l+pgC4dlqOuEV4sc2SFXECgyEgEYAFH6U8eH9TTl5wW5hFhvpiWl\nHSxZA2nMa1ojp97mkufzaGgsJbcB4ni9ydoJyN9Hqs2Wz+JiBtjscGOrmOP1bNyn\nBO9RhHInh0bfTNMrtsIicr4DPNIfM2sl95v6pCDGt7Cfu2NoEnDhId50d73KVONI\n0+rf90WhehEMwoZEzYI0gLmSVbnPEm1j4/OOQfQl7FjaFKyle+A5BWaiRsIqiSue\n0jvZz0DlGmjeHx1yjBIKpq5omOku7aYi4kTNEZKKxzs5HhRFKi2KuYNK/WD5ApQg\ncIhGhhCfAgMBAAECggEANX+LmNjh9VJm/Tigkt4LFxifwgCe8WfKAhNmKHyu5K/3\nIAnzmwxjG5ee8gzNat3pfJk+dCnj7FIHwHScSB6NnCMZZXsV51sVBNC77wMxZIXA\nPyE63fzJEdlt6xvc96k9QweFB1yhs0wJ/6r2JnmcrqxcujBTUA3PIoxcG+TBOc08\ndo5Rcbeq6/3txjGlFM1820WViuFSQQiL6PgNVb+l0JrQ8rAOflKYFOkUb8wux9LX\nnD4vJMwa0j+GRvH5BCcZCguIQZn2JR3rTgcavWtcaHiTNsc49Lsj/hGGOsbkFROo\nGWaSgXE169xiVR/MMEblzqpSXq1qXF2iUeaqyUFIZQKBgQDxxrNlDs1qMfcaQ0S2\nVVtU/f1NfY+kCjQaC4CoYJaaoZINs5ODPs8/2DGnHuhNXMtnPeQ+SzNaK1e1eLbw\nmvq1+n3aGZTvUq2L3b+v7JJ6TQmQ4eBLZBzNjxrxC3EkCULTuROtsAhfzORuE0mE\nwnhR5LpPraEBrPi0re9yDDXVHQKBgQDOCwGw1gNVLh622qR65Zhx5rs2q6ktPxq2\neiUV0KDug6/7QbJzg1pNeoVQmadJR86H0fzKMsN5C7t7z3MIkqXc0+T1NmdN2fPm\ndLthnR1grCDYykoet/CITbAfiip27/o3TJ7YIYItefyZ4GnNH82R/4z3LBDnXB9f\n565hbUj76wKBgEnNMpOFijSBXgFZSU8zDPcLtNeDnWYgazkMC9DZ8v7ulOuzxjKI\n6LB/aOCvsY9z5O712IcfY2SB2HsfhxA47pDADsyVhH3tSeZo4QttdmT4wRPFrza0\nL4qbxUiRCo9KeGiylQwusM+1doEXSBjLV/j/jdOml4AwcZaNhYrVqVUNAoGAU0uD\nzXdXNZJFfGp7X+t9a155hKp05APEyswqPd1vkbzO4eY3PBd35CaJyoGzbR6IUcQE\nS8Gl4ENr8at1t5uBTfqjbrYloQVhYmMCdX3MqI4tYTa2LCD0LkYp0zZJ4Hc3Ui+5\nb2psc/ICujpMy032DvWeiTXZR46oaF8C0gQaIy0CgYEAmKCP4CXmPlWoWqebFp3W\nz2eKWUfASioQ+ZGUVNEge4a6iutciydQJZxBfg9ZXWqDfI0FoRSPfs2zUZFO0AcM\n6oaPGiFnTnH8FGcSHu3p0YysevyoSY6tgsAhb3IiKjJd4e7btsYzpPZbIfyfUVHK\nQFOOSkE+x4J5ts+XO6isQ+w=\n-----END PRIVATE KEY-----\n",
"client_email": "weplayground@weplayground-app.iam.gserviceaccount.com",
"client_id": "106351339097550564510",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/weplayground%40weplayground-app.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

18
env/env.development vendored
View File

@ -22,15 +22,21 @@ CRON_MIDNIGHT="55 11 * * *"
CRON_EVERY_MINUTE="55 11 * * *"
CRON_EVERY_HOUR="0 * * * *"
EMAIL_HOST="sandbox.smtp.mailtrap.io"
EMAIL_HOST=smtp.gmail.com
EMAIL_POST=465
EMAIL_USER="developer@eigen.co.id"
EMAIL_TOKEN="bitqkbkzjzfywxqx"
EMAIL_USER=weplayground.app@gmail.com
EMAIL_TOKEN="sonv vwiu khse vtmv"
// nama email yang akan muncul ke user sebagai pengirim
EMAIL_SENDER=no-reply@eigen.co.id
MIDTRANS_URL=https://app.sandbox.midtrans.com
MIDTRANS_PRODUCTION=false
MIDTRANS_SERVER_KEY=
MIDTRANS_CLIENT_KEY=
MIDTRANS_SERVER_KEY=SB-Mid-server-kH9_RBZrTwaUkxSrC5vOVaeG
MIDTRANS_CLIENT_KEY=SB-Mid-client-7XLwqG5cgjUmZj-7
EXPORT_LIMIT_PARTITION=200
ASSETS="https://asset.sky.eigen.co.id/"
ASSETS="https://asset.sky.eigen.co.id/"
GOOGLE_CALENDAR_KEY="AIzaSyCSg4P3uC9Z7kD1P4f3rf1BbBaz4Q-M55o"
GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec10bd5@group.calendar.google.com"

15
env/env.production vendored
View File

@ -22,15 +22,18 @@ CRON_MIDNIGHT="55 11 * * *"
CRON_EVERY_MINUTE="55 11 * * *"
CRON_EVERY_HOUR="0 * * * *"
EMAIL_HOST="sandbox.smtp.mailtrap.io"
EMAIL_HOST=smtp.gmail.com
EMAIL_POST=465
EMAIL_USER=
EMAIL_TOKEN=
EMAIL_USER=weplayground.app@gmail.com
EMAIL_TOKEN="sonv vwiu khse vtmv"
MIDTRANS_URL=https://app.midtrans.com
MIDTRANS_PRODUCTION=true
MIDTRANS_SERVER_KEY=
MIDTRANS_CLIENT_KEY=
MIDTRANS_SERVER_KEY=Mid-server-BZlPCcrWHDuSxW48oxBs5uAl
MIDTRANS_CLIENT_KEY=Mid-client-YhOPuo0NZPNZfiKq
EXPORT_LIMIT_PARTITION=200
ASSETS="https://asset.sky.eigen.co.id/"
ASSETS="https://asset.sky.eigen.co.id/"
GOOGLE_CALENDAR_KEY="AIzaSyCSg4P3uC9Z7kD1P4f3rf1BbBaz4Q-M55o"
GOOGLE_CALENDAR_ID="326464ac296874c7121825f5ef2e2799baa90b51da240f0045aae22beec10bd5@group.calendar.google.com"

View File

@ -53,6 +53,7 @@
"moment": "^2.30.1",
"nano": "^10.1.3",
"nodemailer": "^6.9.14",
"pdfmake": "^0.2.10",
"pg": "^8.11.5",
"plop": "^4.0.1",
"reflect-metadata": "^0.2.0",

View File

@ -69,6 +69,8 @@ import { NewsModel } from './modules/web-information/news/data/models/news.model
import { BannerModule } from './modules/web-information/banner/banner.module';
import { BannerModel } from './modules/web-information/banner/data/models/banner.model';
import { MailModule } from './modules/configuration/mail/mail.module';
import { PosLogModel } from './modules/configuration/log/data/models/pos-log.model';
import { ExportModule } from './modules/configuration/export/export.module';
@Module({
imports: [
@ -96,6 +98,7 @@ import { MailModule } from './modules/configuration/mail/mail.module';
LogModel,
NewsModel,
PaymentMethodModel,
PosLogModel,
RefundModel,
RefundItemModel,
SalesPriceFormulaModel,
@ -121,6 +124,7 @@ import { MailModule } from './modules/configuration/mail/mail.module';
CqrsModule,
CouchModule,
CronModule,
ExportModule,
GoogleCalendarModule,
LogModule,
MailModule,

View File

@ -25,7 +25,7 @@ export class RolesGuard extends JWTGuard {
if (isNotAllow) {
throw new ForbiddenException({
statusCode: 10003,
message: `Forbidden Access, you don't have access to this module!`,
message: `Akses Terlarang, anda tidak punya akses ke module ini!`,
error: 'ACCESS_FORBIDDEN',
});
}

View File

@ -52,7 +52,7 @@ export class PrivilegeService {
if (!moduleKey) {
throw new ForbiddenException({
statusCode: 10005,
message: `Forbidden Access, access Module is Require!`,
message: `Akses Terlarang, anda tidak punya akses ke module ini!`,
error: 'MODULE_KEY_NOT_FOUND',
});
}

View File

@ -43,9 +43,9 @@ export class CheckDuplicateHelper {
if (data_exists > 0) {
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: `Entity with ${columnCheck.column} : ${
message: `Gagal! Data dengan ${columnCheck.column} : ${
this.entity[columnCheck.column]
} already exist`,
} telah ada`,
error: 'Unprocessable Entity',
});
}

View File

@ -20,12 +20,16 @@ export class SpecificSearchFilter<Entity = any> {
new Brackets((qb) => {
params.forEach((param) => {
const { cols, data, additional, leftJoin } = param;
const columns = cols.split('.');
let arr = data;
const arr = data?.map((el) =>
el.includes("'")
? `'%${el.trim().replace(/'/g, "''").replace(/\s+/g, ' ')}%'`
: `'%${el.trim().replace(/\s+/g, ' ')}%'`,
);
if (!columns.includes('status::text')) {
arr = data?.map((el) =>
el.includes("'")
? `'%${el.trim().replace(/'/g, "''").replace(/\s+/g, ' ')}%'`
: `'%${el.trim().replace(/\s+/g, ' ')}%'`,
);
}
const aliases = !cols.match(/\./g)
? this.table.concat(`.${cols}`)

View File

@ -17,8 +17,8 @@ export abstract class BaseDataService<Entity> {
entityTarget: EntityTarget<Entity>,
entity: Entity,
): Promise<Entity> {
const newEntity = queryRunner.manager.create(entityTarget, entity);
return await queryRunner.manager.save(newEntity);
// const newEntity = this.repository.create(entityTarget, entity);
return await this.repository.save(entity);
}
async createMany(
@ -26,8 +26,8 @@ export abstract class BaseDataService<Entity> {
entityTarget: EntityTarget<Entity>,
entity: Entity[],
): Promise<Entity[]> {
const newEntity = queryRunner.manager.create(entityTarget, entity);
return await queryRunner.manager.save(newEntity);
// const newEntity = this.repository.create(entityTarget, entity);
return await this.repository.save(entity);
}
async createBatch(
@ -35,8 +35,8 @@ export abstract class BaseDataService<Entity> {
entityTarget: EntityTarget<Entity>,
entity: Entity[],
): Promise<Entity[]> {
const newEntity = queryRunner.manager.create(entityTarget, entity);
return await queryRunner.manager.save(newEntity);
// const newEntity = this.repository.create(entityTarget, entity);
return await this.repository.save(entity);
}
async update(
@ -45,13 +45,13 @@ export abstract class BaseDataService<Entity> {
filterUpdate: any,
entity: Entity,
): Promise<Entity> {
const newEntity = await queryRunner.manager.findOne(entityTarget, {
const newEntity = await this.repository.findOne({
where: filterUpdate,
});
if (!newEntity) throw new Error('Data not found!');
Object.assign(newEntity, entity);
return await queryRunner.manager.save(newEntity);
return await this.repository.save(newEntity);
}
async deleteById(
@ -59,7 +59,15 @@ export abstract class BaseDataService<Entity> {
entityTarget: EntityTarget<Entity>,
id: string,
): Promise<void> {
await queryRunner.manager.delete(entityTarget, { id });
await this.repository.delete(id);
}
async deleteByIds(
queryRunner: QueryRunner,
entityTarget: EntityTarget<Entity>,
ids: string[],
): Promise<void> {
await this.repository.delete(ids);
}
async deleteByOptions(
@ -67,11 +75,8 @@ export abstract class BaseDataService<Entity> {
entityTarget: EntityTarget<Entity>,
findManyOptions: FindManyOptions<Entity>,
): Promise<void> {
const datas = await queryRunner.manager.find(entityTarget, findManyOptions);
await queryRunner.manager.delete(
entityTarget,
datas?.map((item) => item['id']),
);
const datas = await this.repository.find(findManyOptions);
await this.repository.delete(datas?.map((item) => item['id']));
}
async getOneByOptions(findOneOptions): Promise<Entity> {

View File

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

View File

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

View File

@ -82,11 +82,11 @@ export abstract class BaseManager {
this.baseLog.verbose('commitTransaction');
await this.queryRunner.commitTransaction();
await this.queryRunner.release();
} catch (e) {
if (e.response) throw new Error(JSON.stringify(e.response));
else throw new Error(e.message);
} finally {
await this.queryRunner.release();
}
}

View File

@ -38,7 +38,7 @@ export abstract class BaseBatchDeleteManager<Entity> extends BaseManager {
if (!entity) {
throw new NotFoundException({
statusCode: HttpStatus.NOT_FOUND,
message: `Failed! Entity with id ${id} not found`,
message: `Gagal! data dengan id ${id} tidak ditemukan`,
error: 'Entity Not Found',
});
}

View File

@ -15,6 +15,26 @@ export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
abstract get entityTarget(): any;
setData(ids: string[], status: STATUS): void {
/**
* // TODO: Handle case confirm multiple tabs;
* Pola ids yang dikirim dirubah menjadi data_id___updated_at
* Untuk mendapatkan value id nya saja dan menghindari breaking change
* karena sudah digunakan sebelumnya lakukan split dari data ids yang dikirim
* Example:
* this.dataIds = ids.map((i)=> {
* return i.split('___')[0]
* })
*
* Simpan data ids yang mempunyai update_at kedalam valiable baru
* Example:
* this.dataIdsWithDate = ids.map((i)=> {
* return {
* id: i.split('___')[0],
* updated_at: i.split('___')[1]
* }
* })
*/
this.dataIds = ids;
this.dataStatus = status;
}
@ -32,8 +52,21 @@ export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
let totalSuccess = 0;
const messages = [];
/**
* // TODO: Handle case confirm multiple tabs;
* Lopping data diambil dari dataIdsWithDate
* exp: for (const item of this.dataIdsWithDate)
*/
for (const id of this.dataIds) {
try {
/**
* // TODO: Handle case confirm multiple tabs;
* buat variable:
* const id = item.id
* const updated_at = item.updated_at
*/
const entity = await this.dataService.getOneByOptions({
where: {
id: id,
@ -44,19 +77,19 @@ export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
if (!entity) {
throw new NotFoundException({
statusCode: HttpStatus.NOT_FOUND,
message: `Failed! Entity with id ${id} not found`,
message: `Gagal! data dengan id ${id} tidak ditemukan`,
error: 'Entity Not Found',
});
}
this.oldData = _.cloneDeep(entity);
await this.validateData(entity);
Object.assign(entity, {
status: this.dataStatus,
editor_id: this.user.id,
editor_name: this.user.name,
updated_at: new Date().getTime(),
});
await this.validateData(entity);
await new ValidateRelationHelper(
id,
this.dataService,
@ -64,6 +97,14 @@ export abstract class BaseBatchUpdateStatusManager<Entity> extends BaseManager {
this.tableName,
).execute();
/**
* // TODO: Handle case confirm multiple tabs;
* lakukan update data dengan where condition id dan updated_at
* EXPECTATION => status akan berubah jika updated_at yang dikirim dari FE sama dengen yang di database
* IF => updated_at beda tidak perlu melakukan update status dan tidak perlu memanggil eventBus tetapi tetap dihitung sebagai aksi yang berhasil
* IF => FE tidak menambahkan updated_at makan lakukan update dan publishEvent
*/
const result = await this.dataService.update(
this.queryRunner,
this.entityTarget,

View File

@ -29,7 +29,7 @@ export abstract class BaseChangePosition<Entity> extends BaseManager {
if (!this.data?.end || this.data.start == this.data?.end) {
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: 'Please drag to another position',
message: 'Gagal! tolong pindahkan ke posisi lain',
error: 'Unprocessable Entity',
});
}
@ -43,7 +43,7 @@ export abstract class BaseChangePosition<Entity> extends BaseManager {
if (!this.startData) {
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: `Entity with id : ${this.data.start} not found`,
message: `Gagal! data dengan id : ${this.data.start} tidak ditemukan`,
error: 'Unprocessable Entity',
});
}
@ -57,7 +57,7 @@ export abstract class BaseChangePosition<Entity> extends BaseManager {
if (!this.endData) {
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: `Entity with id : ${this.data.end} not found`,
message: `Gagal! data dengan id : ${this.data.end} tidak ditemukan`,
error: 'Unprocessable Entity',
});
}

View File

@ -97,7 +97,7 @@ export abstract class BaseCreateManager<Entity> extends BaseManager {
this.eventBus.publishAll([
new topic.topic({
id: data?.['id'] ?? topic?.data?.['id'],
id: this.result['id'],
old: null,
data: data ?? topic.data,
user: this.user,

View File

@ -1,5 +1,6 @@
import { validateRelations } from 'src/core/strings/constants/interface.constants';
import { BaseManager } from '../base.manager';
import { OPERATION } from 'src/core/strings/constants/base.constants';
export abstract class BaseCustomManager<Entity> extends BaseManager {
protected result: any;
@ -23,4 +24,31 @@ export abstract class BaseCustomManager<Entity> extends BaseManager {
}
abstract getResult(): any;
async publishEvents() {
if (!this.eventTopics.length) return;
for (const topic of this.eventTopics) {
let data;
if (!topic.data) {
data = await this.dataService.getOneByOptions({
where: {
id: this.result['id'],
},
relations: topic.relations,
});
}
this.eventBus.publishAll([
new topic.topic({
id: data?.['id'] ?? topic?.data?.['id'],
old: null,
data: data ?? topic.data,
user: this.user,
description: '',
module: this.tableName,
op: OPERATION.UPDATE,
}),
]);
}
}
}

View File

@ -23,7 +23,7 @@ export abstract class BaseDeleteManager<Entity> extends BaseManager {
if (!this.data)
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
message: `Data with id ${this.dataId} not found`,
message: `Gagal! Data denga id ${this.dataId} tidak ditemukan`,
error: 'Unprocessable Entity',
});

View File

@ -50,12 +50,16 @@ export abstract class BaseIndexManager<Entity> extends BaseReadManager {
// jika searching status terdapat dalam enum, maka dia mencari specific data
// ? karena jika tidak, ketika dia search "active" maka "inactive" juga ikut
return STATUS[statusData.toUpperCase()] ?? statusData;
});
specificFilter.push({
cols: `${this.tableName}.status::text`,
data: data,
return `'${STATUS[statusData.toUpperCase()]}'` ?? `'%${statusData}%'`;
});
const exist = specificFilter.find((item) => item.isStatus);
if (!exist) {
specificFilter.push({
cols: `${this.tableName}.status::text`,
data: data,
});
}
}
new SpecificSearchFilter<Entity>(

View File

@ -14,6 +14,22 @@ export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
abstract get entityTarget(): any;
setData(id: string, status: STATUS): void {
/**
* // TODO: Handle case confirm multiple tabs;
* Pola id yang dikirim dirubah menjadi data_id___updated_at
* Untuk mendapatkan value id nya saja dan menghindari breaking change
* karena sudah digunakan sebelumnya lakukan split dari data ids yang dikirim
* Example:
* this.dataId = id.split('___')[0]
*
* Simpan data id yang mempunyai update_at kedalam valiable baru
* Example:
* this.dataIdsWithDate = {
* id: id.split('___')[0],
* updated_at: id.split('___')[1]
* }
*/
this.dataId = id;
this.dataStatus = status;
}
@ -43,6 +59,16 @@ export abstract class BaseUpdateStatusManager<Entity> extends BaseManager {
this.tableName,
).execute();
/**
* // TODO: Handle case confirm multiple tabs;
* IF => updated_at sama dengen data yang di database
* THEN =>
* - Lakukan update data dengan where condition id dan updated_at
* - EXPECTATION = > status akan berubah jika updated_at yang dikirim dari FE sama dengen yang di database
* IF => updated_at beda maka retun curent data tanpa harus malakukan update status dan publish event
* IF => FE tidak menambahkan updated_at makan lakukan update dan publishEvent
*/
this.result = await this.dataService.update(
this.queryRunner,
this.entityTarget,

View File

@ -27,7 +27,7 @@ export abstract class BaseUpdateManager<Entity> extends BaseManager {
if (!this.oldData) {
throw new NotFoundException({
statusCode: HttpStatus.NOT_FOUND,
message: `Failed! Entity with id ${this.dataId} not found`,
message: `Gagal! Data denga id ${this.dataId} tidak ditemukan`,
error: 'Entity Not Found',
});
}
@ -102,7 +102,7 @@ export abstract class BaseUpdateManager<Entity> extends BaseManager {
this.eventBus.publishAll([
new topic.topic({
id: data?.['id'] ?? topic?.data?.['id'],
id: topic.data?.['id'] ?? this.dataId,
old: this.oldData,
data: data ?? topic.data,
user: this.user,

View File

@ -20,6 +20,16 @@ export enum ORDER_TYPE {
DESC = 'DESC',
}
export const DAY = [
'minggu',
'senin',
'selasa',
'rabu',
'kamis',
'jumat',
'sabtu',
];
export enum CONNECTION_NAME {
DEFAULT = 'default',
}

View File

@ -51,7 +51,6 @@ export const PrivilegeAdminConstant = [
menu_label: 'Rekonsiliasi',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.CONFIRM,
PrivilegeAction.DELETE,
PrivilegeAction.CANCEL,
@ -137,6 +136,12 @@ export const PrivilegeAdminConstant = [
actions: [PrivilegeAction.CREATE],
index: 13,
},
{
menu: 'DOWNLOAD_POS_APP',
menu_label: 'Download POS App',
actions: [PrivilegeAction.VIEW],
index: 20,
},
];
export const PrivilegePOSConstant = [
@ -146,8 +151,8 @@ export const PrivilegePOSConstant = [
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.DELETE,
PrivilegeAction.EDIT,
PrivilegeAction.CANCEL,
],
index: 14,
},
@ -160,7 +165,7 @@ export const PrivilegePOSConstant = [
{
menu: 'BOOKING',
menu_label: 'Pemesanan',
actions: [PrivilegeAction.VIEW, PrivilegeAction.CREATE],
actions: [PrivilegeAction.VIEW],
index: 16,
},
{
@ -175,4 +180,10 @@ export const PrivilegePOSConstant = [
actions: [PrivilegeAction.CREATE],
index: 18,
},
{
menu: 'PRINT_RECEIPT',
menu_label: 'Print Receipt',
actions: [PrivilegeAction.CREATE],
index: 19,
},
];

View File

@ -7,6 +7,7 @@ export enum TABLE_NAME {
ITEM_RATE = 'item_rates',
GATE = 'gates',
LOG = 'logs',
LOG_POS = 'logs_pos',
NEWS = 'news',
PAYMENT_METHOD = 'payment_methods',
PRICE_FORMULA = 'price_formulas',

View File

@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class PosLog1721736523991 implements MigrationInterface {
name = 'PosLog1721736523991';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "logs_pos" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying NOT NULL DEFAULT 'cash withdrawal', "pos_number" bigint, "total_balance" numeric, "created_at" bigint, "creator_name" character varying, "creator_id" character varying, CONSTRAINT "PK_60df825558a6b6881d7ad770d26" PRIMARY KEY ("id"))`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "logs_pos"`);
}
}

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCalendarColumnTransaction1721892389807
implements MigrationInterface
{
name = 'AddCalendarColumnTransaction1721892389807';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "transactions" ADD "calendar_id" character varying`,
);
await queryRunner.query(
`ALTER TABLE "transactions" ADD "calendar_link" character varying`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "transactions" DROP COLUMN "calendar_id"`,
);
await queryRunner.query(
`ALTER TABLE "transactions" DROP COLUMN "calendar_link"`,
);
}
}

View File

@ -0,0 +1,27 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddColumnToRefundTable1722318939681 implements MigrationInterface {
name = 'AddColumnToRefundTable1722318939681';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "public"."refunds_refund_reason_type_enum" AS ENUM('weather', 'ride malfunction', 'other')`,
);
await queryRunner.query(
`ALTER TABLE "refunds" ADD "refund_reason_type" "public"."refunds_refund_reason_type_enum" NOT NULL DEFAULT 'ride malfunction'`,
);
await queryRunner.query(`ALTER TABLE "refunds" ADD "refund_reason" text`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "refunds" DROP COLUMN "refund_reason"`,
);
await queryRunner.query(
`ALTER TABLE "refunds" DROP COLUMN "refund_reason_type"`,
);
await queryRunner.query(
`DROP TYPE "public"."refunds_refund_reason_type_enum"`,
);
}
}

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateColumnToTransactionTable1722334034920
implements MigrationInterface
{
name = 'UpdateColumnToTransactionTable1722334034920';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "transaction_items" ADD "qr_image_url" character varying`,
);
await queryRunner.query(
`ALTER TABLE "transactions" ADD "payment_code" character varying`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "transactions" DROP COLUMN "payment_code"`,
);
await queryRunner.query(
`ALTER TABLE "transaction_items" DROP COLUMN "qr_image_url"`,
);
}
}

View File

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class PosLogAddColumnDownBy1722509262047 implements MigrationInterface {
name = 'PosLogAddColumnDownBy1722509262047';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "logs_pos" ADD "drawn_by_name" character varying`,
);
await queryRunner.query(
`ALTER TABLE "logs_pos" ADD "drawn_by_id" character varying`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "logs_pos" DROP COLUMN "drawn_by_id"`);
await queryRunner.query(
`ALTER TABLE "logs_pos" DROP COLUMN "drawn_by_name"`,
);
}
}

View File

@ -0,0 +1,49 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateRelationTableTransaction1722581313837
implements MigrationInterface
{
name = 'UpdateRelationTableTransaction1722581313837';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "refunds" DROP CONSTRAINT "FK_8bb3b7579f49990d2e77684acd4"`,
);
await queryRunner.query(
`ALTER TABLE "refunds" DROP CONSTRAINT "REL_8bb3b7579f49990d2e77684acd"`,
);
await queryRunner.query(
`ALTER TABLE "refund_items" DROP CONSTRAINT "FK_07b481a163c219f5de8fb1c90b3"`,
);
await queryRunner.query(
`ALTER TABLE "refund_items" DROP CONSTRAINT "REL_07b481a163c219f5de8fb1c90b"`,
);
await queryRunner.query(
`ALTER TABLE "refunds" ADD CONSTRAINT "FK_8bb3b7579f49990d2e77684acd4" FOREIGN KEY ("transaction_id") REFERENCES "transactions"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "refund_items" ADD CONSTRAINT "FK_07b481a163c219f5de8fb1c90b3" FOREIGN KEY ("transaction_item_id") REFERENCES "transaction_items"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "refund_items" DROP CONSTRAINT "FK_07b481a163c219f5de8fb1c90b3"`,
);
await queryRunner.query(
`ALTER TABLE "refunds" DROP CONSTRAINT "FK_8bb3b7579f49990d2e77684acd4"`,
);
await queryRunner.query(
`ALTER TABLE "refund_items" ADD CONSTRAINT "REL_07b481a163c219f5de8fb1c90b" UNIQUE ("transaction_item_id")`,
);
await queryRunner.query(
`ALTER TABLE "refund_items" ADD CONSTRAINT "FK_07b481a163c219f5de8fb1c90b3" FOREIGN KEY ("transaction_item_id") REFERENCES "transaction_items"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "refunds" ADD CONSTRAINT "REL_8bb3b7579f49990d2e77684acd" UNIQUE ("transaction_id")`,
);
await queryRunner.query(
`ALTER TABLE "refunds" ADD CONSTRAINT "FK_8bb3b7579f49990d2e77684acd4" FOREIGN KEY ("transaction_id") REFERENCES "transactions"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
}

View File

@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateTypeColumnItemTable1722587128195
implements MigrationInterface
{
name = 'UpdateTypeColumnItemTable1722587128195';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "items" DROP COLUMN "sales_margin"`);
await queryRunner.query(`ALTER TABLE "items" ADD "sales_margin" numeric`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "items" DROP COLUMN "sales_margin"`);
await queryRunner.query(`ALTER TABLE "items" ADD "sales_margin" integer`);
}
}

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateTableTransaction1722595038215 implements MigrationInterface {
name = 'UpdateTableTransaction1722595038215';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "public"."transactions_payment_type_counter_enum" AS ENUM('midtrans', 'bank transfer', 'qris', 'counter', 'cash', 'credit card', 'debit', 'e-money')`,
);
await queryRunner.query(
`ALTER TABLE "transactions" ADD "payment_type_counter" "public"."transactions_payment_type_counter_enum"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "transactions" DROP COLUMN "payment_type_counter"`,
);
await queryRunner.query(
`DROP TYPE "public"."transactions_payment_type_counter_enum"`,
);
}
}

View File

@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddColumnToTransactionsTable1722693550579
implements MigrationInterface
{
name = 'AddColumnToTransactionsTable1722693550579';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "transactions" ADD "booking_date_before" date`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "transactions" DROP COLUMN "booking_date_before"`,
);
}
}

View File

@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UniqueNameItem1722922766205 implements MigrationInterface {
name = 'UniqueNameItem1722922766205';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "item_bundlings" DROP CONSTRAINT "FK_a50e7abf2caba4d0394f3726b86"`,
);
await queryRunner.query(
`ALTER TABLE "items" ADD CONSTRAINT "UQ_213736582899b3599acaade2cd1" UNIQUE ("name")`,
);
await queryRunner.query(
`ALTER TABLE "item_bundlings" ADD CONSTRAINT "FK_a50e7abf2caba4d0394f3726b86" FOREIGN KEY ("item_bundling_id") REFERENCES "items"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "item_bundlings" DROP CONSTRAINT "FK_a50e7abf2caba4d0394f3726b86"`,
);
await queryRunner.query(
`ALTER TABLE "items" DROP CONSTRAINT "UQ_213736582899b3599acaade2cd1"`,
);
await queryRunner.query(
`ALTER TABLE "item_bundlings" ADD CONSTRAINT "FK_a50e7abf2caba4d0394f3726b86" FOREIGN KEY ("item_bundling_id") REFERENCES "items"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
}

View File

@ -133,7 +133,7 @@ export class LoginManager extends BaseCustomManager<UserEntity> {
throwError() {
throw new UnauthorizedException({
statusCode: HttpStatus.UNAUTHORIZED,
message: `Failed! You have entered an invalid username or password`,
message: `Gagal! username atau password tidak sesuai`,
error: 'Unauthorized',
});
}

View File

@ -5,8 +5,12 @@ import { STATUS } from 'src/core/strings/constants/base.constants';
import { ItemType } from 'src/modules/item-related/item-category/constants';
import { LimitType } from 'src/modules/item-related/item/constants';
import { PaymentMethodType } from 'src/modules/transaction/payment-method/constants';
import { RefundType } from 'src/modules/transaction/refund/constants';
import {
RefundReasonType,
RefundType,
} from 'src/modules/transaction/refund/constants';
import { GateType } from 'src/modules/web-information/gate/constants';
import { InvoiceType } from '../../export/constants';
@ApiTags('configuration - constant')
@Controller('v1/constant')
@ -52,8 +56,18 @@ export class ConstantController {
return Object.values(RefundType);
}
@Get('refund-reason-type')
async refundReasonType(): Promise<any> {
return Object.values(RefundReasonType);
}
@Get('gate-type')
async gateType(): Promise<any> {
return Object.values(GateType);
}
@Get('invoice-type')
async invoiceType(): Promise<any> {
return Object.values(InvoiceType);
}
}

View File

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

View File

@ -17,10 +17,13 @@ import {
} from './domain/managers/season-period.handler';
import {
ItemDeletedHandler,
ItemPriceUpdatedHandler,
ItemRateUpdatedHandler,
ItemUpdatedHandler,
} from './domain/managers/item.handler';
import {
UserDeletedHandler,
UserPrivilegeUpdateHandler,
UserUpdatedHandler,
} from './domain/managers/user.handler';
import { TypeOrmModule } from '@nestjs/typeorm';
@ -31,13 +34,22 @@ import { UserDataService } from 'src/modules/user-related/user/data/services/use
import { ItemDataService } from 'src/modules/item-related/item/data/services/item-data.service';
import {
BookingDeletedEvent,
BookingHandler,
// BookingHandler,
BookingUpdateHandler,
ChangeStatusBookingHandler,
} from './domain/managers/booking.handler';
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
import { TransactionTaxModel } from 'src/modules/transaction/transaction/data/models/transaction-tax.model';
import { TransactionItemModel } from 'src/modules/transaction/transaction/data/models/transaction-item.model';
import { VipCodeCreatedHandler } from './domain/managers/vip-code.handler';
import { ItemRateModel } from 'src/modules/item-related/item-rate/data/models/item-rate.model';
import {
SeasonTypeDeletedHandler,
SeasonTypeUpdatedHandler,
} from './domain/managers/season-type.handler';
import { SeasonPeriodDataService } from 'src/modules/season-related/season-period/data/services/season-period-data.service';
import { SeasonPeriodModel } from 'src/modules/season-related/season-period/data/models/season-period.model';
@Module({
imports: [
@ -45,6 +57,8 @@ import { VipCodeCreatedHandler } from './domain/managers/vip-code.handler';
TypeOrmModule.forFeature(
[
ItemModel,
ItemRateModel,
SeasonPeriodModel,
UserModel,
TransactionModel,
TransactionTaxModel,
@ -56,7 +70,9 @@ import { VipCodeCreatedHandler } from './domain/managers/vip-code.handler';
],
controllers: [CouchDataController],
providers: [
BookingHandler,
// BookingHandler,
BookingUpdateHandler,
ChangeStatusBookingHandler,
BookingDeletedEvent,
PaymentMethodDeletedHandler,
PaymentMethodUpdatedHandler,
@ -67,9 +83,16 @@ import { VipCodeCreatedHandler } from './domain/managers/vip-code.handler';
SeasonPeriodUpdatedHandler,
ItemUpdatedHandler,
ItemDeletedHandler,
ItemRateUpdatedHandler,
ItemPriceUpdatedHandler,
UserDeletedHandler,
UserUpdatedHandler,
UserPrivilegeUpdateHandler,
SeasonTypeDeletedHandler,
SeasonTypeUpdatedHandler,
SeasonPeriodDataService,
TransactionDataService,
UserDataService,
ItemDataService,

View File

@ -3,7 +3,7 @@ import { DatabaseListen } from '../../constants';
import { EventBus } from '@nestjs/cqrs';
import { ChangeDocEvent } from '../../domain/events/change-doc.event';
import { ConfigService } from '@nestjs/config';
import { apm } from 'src/core/apm';
import * as Nano from 'nano';
@Injectable()
@ -23,6 +23,10 @@ export class CouchService {
for (const database of DatabaseListen) {
const db = nano.db.use(database);
db.changesReader.start({ includeDocs: true }).on('change', (change) => {
Logger.log(
`Receive Data from ${database}: ${change?.id}`,
'CouchService',
);
this.changeDoc(change, database);
});
@ -45,7 +49,10 @@ export class CouchService {
const nano = this.nanoInstance;
const db = nano.use(database);
return await db.insert(data);
} catch (error) {}
} catch (error) {
console.log(error);
apm.captureError(error);
}
}
public async deleteDoc(data, database) {
@ -54,7 +61,10 @@ export class CouchService {
const db = nano.use(database);
const result = await db.get(data.id);
await db.destroy(data.id, result._rev);
} catch (error) {}
} catch (error) {
console.log(error);
apm.captureError(error);
}
}
public async updateDoc(data, database) {
@ -62,11 +72,26 @@ export class CouchService {
const nano = this.nanoInstance;
const db = nano.use(database);
const result = await db.get(data.id);
console.log(result, 'dsa');
await db.insert({
...data,
_rev: result._rev,
});
} catch (error) {}
} catch (error) {
console.log(error);
apm.captureError(error);
}
}
public async getDoc(id: string, database: string) {
try {
const nano = this.nanoInstance;
const db = nano.use(database);
const result = await db.get(id);
return result;
} catch (error) {
console.log(error);
apm.captureError(error);
return null;
}
}
}

View File

@ -2,8 +2,7 @@ import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
import { TransactionChangeStatusEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event';
import { CouchService } from '../../data/services/couch.service';
import { STATUS } from 'src/core/strings/constants/base.constants';
import { TransactionPaymentType } from 'src/modules/transaction/transaction/constants';
// import { STATUS } from 'src/core/strings/constants/base.constants';
import { mappingTransaction } from 'src/modules/transaction/transaction/domain/usecases/managers/helpers/mapping-transaction.helper';
import { TransactionDeletedEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-deleted.event';
import { TransactionUpdatedEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-updated.event';
@ -20,13 +19,65 @@ export class BookingDeletedEvent
_id: event.data.id,
...event.data.data,
},
'item',
'booking',
);
}
}
@EventsHandler(TransactionChangeStatusEvent, TransactionUpdatedEvent)
export class BookingHandler
// @EventsHandler(TransactionChangeStatusEvent, TransactionUpdatedEvent)
// export class BookingHandler
// implements IEventHandler<TransactionChangeStatusEvent>
// {
// constructor(
// private bookingService: TransactionDataService,
// private couchService: CouchService,
// ) {}
// async handle(event: TransactionChangeStatusEvent) {
// const old_data = event.data.old;
// const data = event.data.data;
// if (
// data.payment_type == TransactionPaymentType.COUNTER ||
// ([STATUS.ACTIVE, STATUS.SETTLED].includes(data.status) &&
// data.payment_type != TransactionPaymentType.COUNTER)
// ) {
// const booking = await this.bookingService.getOneByOptions({
// where: {
// id: data.id,
// },
// relations: ['items'],
// });
// mappingTransaction(booking);
// if (
// (old_data?.status != data.status ||
// data.payment_type != TransactionPaymentType.COUNTER) &&
// [STATUS.PENDING, STATUS.ACTIVE, STATUS.SETTLED].includes(data.status)
// ) {
// await this.couchService.createDoc(
// {
// _id: booking.id,
// ...booking,
// },
// 'booking',
// );
// } else {
// await this.couchService.updateDoc(
// {
// _id: booking.id,
// ...booking,
// },
// 'booking',
// );
// }
// }
// }
// }
@EventsHandler(TransactionChangeStatusEvent)
export class ChangeStatusBookingHandler
implements IEventHandler<TransactionChangeStatusEvent>
{
constructor(
@ -35,24 +86,23 @@ export class BookingHandler
) {}
async handle(event: TransactionChangeStatusEvent) {
const old_data = event.data.old;
const data = event.data.data;
const dataID = data?.id ?? data?.order_id;
if (data.payment_type != TransactionPaymentType.COUNTER) return;
const couchData = await this.couchService.getDoc(dataID, 'booking');
const booking = await this.bookingService.getOneByOptions({
where: {
id: data.id,
id: dataID,
},
relations: ['items'],
});
console.log('change status', { dataID, couchData, booking });
mappingTransaction(booking);
console.log('after mapping');
if (
old_data?.status != data.status &&
[STATUS.PENDING, STATUS.ACTIVE].includes(data.status)
) {
if (!couchData) {
console.log('save data to couch');
await this.couchService.createDoc(
{
_id: booking.id,
@ -61,6 +111,42 @@ export class BookingHandler
'booking',
);
} else {
console.log('update data to couch');
await this.couchService.updateDoc(
{
_id: booking.id,
...booking,
},
'booking',
);
}
}
}
@EventsHandler(TransactionUpdatedEvent)
export class BookingUpdateHandler
implements IEventHandler<TransactionUpdatedEvent>
{
constructor(
private bookingService: TransactionDataService,
private couchService: CouchService,
) {}
async handle(event: TransactionUpdatedEvent) {
const data = event.data.data;
const dataID = data?.id ?? data?.order_id;
const couchData = await this.couchService.getDoc(dataID, 'booking');
console.log('update', { dataID, couchData });
if (couchData) {
const booking = await this.bookingService.getOneByOptions({
where: {
id: dataID,
},
relations: ['items'],
});
console.log({ booking });
mappingTransaction(booking);
await this.couchService.updateDoc(
{
_id: booking.id,

View File

@ -5,6 +5,9 @@ import { ItemDeletedEvent } from 'src/modules/item-related/item/domain/entities/
import { ItemUpdatedEvent } from 'src/modules/item-related/item/domain/entities/event/item-updated.event';
import { ItemChangeStatusEvent } from 'src/modules/item-related/item/domain/entities/event/item-change-status.event';
import { ItemDataService } from 'src/modules/item-related/item/data/services/item-data.service';
import { SeasonPeriodUpdatedEvent } from 'src/modules/season-related/season-period/domain/entities/event/season-period-updated.event';
import { SeasonPeriodChangeStatusEvent } from 'src/modules/season-related/season-period/domain/entities/event/season-period-change-status.event';
import { ItemRateUpdatedEvent } from 'src/modules/item-related/item-rate/domain/entities/event/item-rate-updated.event';
@EventsHandler(ItemDeletedEvent)
export class ItemDeletedHandler implements IEventHandler<ItemDeletedEvent> {
@ -79,3 +82,85 @@ export class ItemUpdatedHandler
}
}
}
@EventsHandler(SeasonPeriodChangeStatusEvent, SeasonPeriodUpdatedEvent)
export class ItemPriceUpdatedHandler
implements IEventHandler<SeasonPeriodChangeStatusEvent>
{
constructor(
private couchService: CouchService,
private itemService: ItemDataService,
) {}
async handle(event: SeasonPeriodChangeStatusEvent) {
const data = event.data.data;
// change status to active
if (data.status == STATUS.ACTIVE) {
const dataItems = await this.itemService.getManyByOptions({
where: {
status: STATUS.ACTIVE,
},
relations: [
'item_category',
'bundling_items',
'bundling_items.item_category',
'item_rates',
'item_rates.item',
'item_rates.season_period',
'item_rates.season_period.season_type',
],
});
for (const dataItem of dataItems) {
await this.couchService.updateDoc(
{
_id: dataItem.id,
...dataItem,
},
'item',
);
}
}
}
}
@EventsHandler(ItemRateUpdatedEvent)
export class ItemRateUpdatedHandler
implements IEventHandler<ItemRateUpdatedEvent>
{
constructor(
private couchService: CouchService,
private itemService: ItemDataService,
) {}
async handle(event: ItemRateUpdatedEvent) {
const data = event.data.data;
const dataItems = await this.itemService.getManyByOptions({
where: {
status: STATUS.ACTIVE,
id: data.item?.id,
},
relations: [
'item_category',
'bundling_items',
'bundling_items.item_category',
'item_rates',
'item_rates.item',
'item_rates.season_period',
'item_rates.season_period.season_type',
],
});
for (const dataItem of dataItems) {
await this.couchService.updateDoc(
{
_id: dataItem.id,
...dataItem,
},
'item',
);
}
}
}

View File

@ -0,0 +1,52 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { CouchService } from '../../data/services/couch.service';
import { SeasonTypeDeletedEvent } from 'src/modules/season-related/season-type/domain/entities/event/season-type-deleted.event';
import { SeasonTypeChangeStatusEvent } from 'src/modules/season-related/season-type/domain/entities/event/season-type-change-status.event';
import { SeasonTypeUpdatedEvent } from 'src/modules/season-related/season-type/domain/entities/event/season-type-updated.event';
import { SeasonPeriodDataService } from 'src/modules/season-related/season-period/data/services/season-period-data.service';
@EventsHandler(SeasonTypeDeletedEvent)
export class SeasonTypeDeletedHandler
implements IEventHandler<SeasonTypeDeletedEvent>
{
constructor(private couchService: CouchService) {}
async handle(event: SeasonTypeDeletedEvent) {
console.log('deleted session type');
}
}
@EventsHandler(SeasonTypeChangeStatusEvent, SeasonTypeUpdatedEvent)
export class SeasonTypeUpdatedHandler
implements IEventHandler<SeasonTypeChangeStatusEvent>
{
constructor(
private couchService: CouchService,
private seasonPeriodService: SeasonPeriodDataService,
) {}
async handle(event: SeasonTypeChangeStatusEvent) {
const data = event.data.data;
const typeID = data.id;
const periods = await this.seasonPeriodService.getManyByOptions({
where: {
season_type_id: typeID,
},
relations: ['season_type'],
});
for (const period of periods) {
const dataID = period.id;
const couchData = await this.couchService.getDoc(dataID, 'season_period');
if (couchData) {
await this.couchService.updateDoc(
{
_id: dataID,
...period,
},
'season_period',
);
}
}
}
}

View File

@ -5,6 +5,7 @@ import { UserDeletedEvent } from 'src/modules/user-related/user/domain/entities/
import { UserChangeStatusEvent } from 'src/modules/user-related/user/domain/entities/event/user-change-status.event';
import { UserUpdatedEvent } from 'src/modules/user-related/user/domain/entities/event/user-updated.event';
import { UserDataService } from 'src/modules/user-related/user/data/services/user-data.service';
import { UserPrivilegeConfigUpdatedEvent } from 'src/modules/user-related/user-privilege/domain/entities/event/user-privilege-configuration-updated.event';
@EventsHandler(UserDeletedEvent)
export class UserDeletedHandler implements IEventHandler<UserDeletedEvent> {
@ -44,14 +45,16 @@ export class UserUpdatedHandler
],
})
.then((item) => {
const user_privilege_configurations = item[
'user_privilege'
]?.user_privilege_configurations?.filter(
(config) => config.module == 'POS',
);
Object.assign(item['user_privilege'], {
user_privilege_configurations: user_privilege_configurations,
});
if (item.role != 'superadmin') {
const user_privilege_configurations = item[
'user_privilege'
]?.user_privilege_configurations?.filter(
(config) => config.module == 'POS',
);
Object.assign(item['user_privilege'], {
user_privilege_configurations: user_privilege_configurations,
});
}
return item;
});
@ -86,3 +89,52 @@ export class UserUpdatedHandler
}
}
}
@EventsHandler(UserPrivilegeConfigUpdatedEvent)
export class UserPrivilegeUpdateHandler
implements IEventHandler<UserPrivilegeConfigUpdatedEvent>
{
constructor(
private couchService: CouchService,
private userService: UserDataService,
) {}
async handle(event: UserPrivilegeConfigUpdatedEvent) {
const data = event.data.data;
const users = await this.userService
.getManyByOptions({
where: {
user_privilege_id: data.user_privilege_id ?? data.id,
status: STATUS.ACTIVE,
},
relations: [
'user_privilege',
'user_privilege.user_privilege_configurations',
],
})
.then((items) => {
return items?.map((item) => {
const user_privilege_configurations = item[
'user_privilege'
]?.user_privilege_configurations?.filter(
(config) => config.module == 'POS',
);
Object.assign(item['user_privilege'], {
user_privilege_configurations: user_privilege_configurations,
});
return item;
});
});
for (const user of users) {
await this.couchService.updateDoc(
{
_id: user.id,
...user,
},
'user',
);
}
}
}

View File

@ -0,0 +1,10 @@
export enum InvoiceType {
BOOKING_INVOICE = 'this is your invoice',
PAYMENT_CONFIRMATION = 'payment confirmation',
INVOICE_EXPIRED = 'invoice has expired',
REFUND_REQUEST = 'your refund request',
REFUND_CONFIRMATION = 'refund confirmation',
BOOKING_DATE_CHANGE = 'booking date change',
}
export const PhoneNumber = '088';

View File

@ -0,0 +1,55 @@
import { Injectable } from '@nestjs/common';
import { BaseCustomManager } from 'src/core/modules/domain/usecase/managers/base-custom.manager';
import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity';
import { EventTopics } from 'src/core/strings/constants/interface.constants';
import { STATUS } from 'src/core/strings/constants/base.constants';
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
import { GeneratePdf } from '../templates/helpers/generate-pdf.helper';
@Injectable()
export class PdfMakeManager extends BaseCustomManager<TransactionEntity> {
get entityTarget(): any {
return TransactionModel;
}
get eventTopics(): EventTopics[] {
return [];
}
async validateProcess(): Promise<void> {
return;
}
async beforeProcess(): Promise<void> {
return;
}
async process(): Promise<void> {
try {
const transaction = await this.dataService.getOneByOptions({
where: {
id: this.data.id,
},
relations: ['items'],
});
const banks = await this.dataServiceFirstOpt.getManyByOptions({
where: {
status: STATUS.ACTIVE,
},
});
this.result = GeneratePdf(transaction, this.data.invoice_type, banks);
} catch (error) {
console.log(error, 'generate pdf');
}
return;
}
async afterProcess(): Promise<void> {
return;
}
getResult() {
return this.result;
}
}

View File

@ -0,0 +1,43 @@
import { InvoiceTemplate } from '../invoice.template';
import * as PdfPrinter from 'pdfmake';
import { PassThrough } from 'stream';
export async function GeneratePdf(transaction, invoiceType, banks) {
const fonts = {
Roboto: {
normal: './assets/fonts/Roboto-Regular.ttf',
bold: './assets/fonts/Roboto-Medium.ttf',
italics: './assets/fonts/Roboto-Italic.ttf',
bolditalics: './assets/fonts/Roboto-MediumItalic.ttf',
},
};
const printer = new PdfPrinter(fonts);
const docDefinition = InvoiceTemplate(transaction, invoiceType, banks);
const createPdfBuffer = (docDefinition) => {
return new Promise((resolve, reject) => {
const pdfDoc = printer.createPdfKitDocument(docDefinition);
const chunks = [];
const stream = new PassThrough();
pdfDoc.pipe(stream);
pdfDoc.end();
stream.on('data', (chunk) => {
chunks.push(chunk);
});
stream.on('end', () => {
const pdfBuffer = Buffer.concat(chunks);
resolve(pdfBuffer);
});
stream.on('error', reject);
});
};
const pdfBuffer = await createPdfBuffer(docDefinition);
return pdfBuffer;
}

View File

@ -0,0 +1,443 @@
import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity';
import { InvoiceType, PhoneNumber } from '../../../constants';
import { PaymentMethodEntity } from 'src/modules/transaction/payment-method/domain/entities/payment-method.entity';
export function mappingHeader(transaction: TransactionEntity, invoiceType) {
return [
{
stack: [
{
alignment: 'center',
columns: [
{
width: 100,
image: 'data',
},
{
width: 'auto',
text: 'have',
fontSize: 50,
bold: true,
color: '#169f54',
padding: [0, 0, 0, 0],
margin: [0, 0, 0, 0],
alignment: 'center',
},
{
width: 'auto',
text: 'fun',
fontSize: 50,
bold: true,
color: '#61c4eb',
padding: [0, 0, 0, 0],
margin: [0, 0, 0, 0],
// alignment: "center"
},
],
},
{
text: invoiceType.toUpperCase(),
fontSize: 30,
alignment: 'center',
},
{
columns: [
{
text: transaction.invoice_code,
},
{
text: new Date(transaction.booking_date).toDateString(),
alignment: 'right',
},
],
},
],
},
{
width: 150,
text: 'Jl, Kolonel Masturi No.KM. 11, \n Kertawangi, Kec. Cisatua, \n Kab. Bandung Barat, \n Jawa Barat 40551 \n 0815-6380-8021',
// color: '#aaaaab',
// bold: true,
fontSize: 11,
margin: [0, 20, 0, 5],
},
];
}
export function mappingFooter() {
return [
[
{
text: 'we commits to prividing an educative, \n playful, and purposeful environment\n this is advantageous for all ages',
border: [false, true, false, false],
margin: [0, 5, 0, 5],
alignment: 'right',
},
{
text: 'Thank \n You',
border: [false, true, false, false],
fontSize: 20,
bold: true,
color: '#169f54',
margin: [0, 5, 0, 5],
},
],
];
}
export function mappingPrice(
transaction: TransactionEntity,
invoiceType: InvoiceType,
) {
const result = [];
const totalData = [
InvoiceType.REFUND_CONFIRMATION,
InvoiceType.REFUND_REQUEST,
].includes(invoiceType)
? transaction['refund'].refund_total
: Number(transaction.payment_total).toLocaleString('id-ID', {
style: 'currency',
currency: 'IDR',
});
const subTotalData = [
InvoiceType.REFUND_CONFIRMATION,
InvoiceType.REFUND_REQUEST,
].includes(invoiceType)
? transaction['refund'].refund_total
: Number(transaction.payment_sub_total).toLocaleString('id-ID', {
style: 'currency',
currency: 'IDR',
});
const sub_total = [
{
text: 'SUBTOTAL',
alignment: 'right',
bold: true,
margin: [0, 5, 0, 5],
},
{
text: subTotalData,
margin: [0, 5, 0, 5],
},
];
result.push(sub_total);
if (Number(transaction.payment_discount_total ?? 0) > 0) {
const discount = [
{
text: 'DISCOUNT',
bold: true,
alignment: 'right',
margin: [0, 5, 0, 5],
},
{
text: Number(transaction.payment_discount_total).toLocaleString(
'id-ID',
{
style: 'currency',
currency: 'IDR',
},
),
margin: [0, 5, 0, 5],
},
];
result.push(discount);
}
const total = [
{
text: 'TOTAL',
bold: true,
border: [false, false, false, true],
alignment: 'right',
margin: [0, 5, 0, 5],
},
{
text: totalData,
border: [false, false, false, true],
margin: [0, 5, 0, 5],
},
];
result.push(total);
return result;
}
export function mappingItem(transaction, invoiceType: InvoiceType) {
const header = [
{
text: 'ITEM',
border: [false, true, false, false],
margin: [0, 5, 0, 5],
bold: true,
textTransform: 'uppercase',
},
{
text: 'QTY',
alignment: 'center',
border: [false, true, false, false],
margin: [0, 5, 0, 5],
bold: true,
textTransform: 'uppercase',
},
{
text: 'PRICE',
alignment: 'center',
border: [false, true, false, false],
margin: [0, 5, 0, 5],
bold: true,
textTransform: 'uppercase',
},
{
text: 'AMOUNT',
border: [false, true, false, false],
alignment: 'center',
bold: true,
margin: [0, 5, 0, 5],
textTransform: 'uppercase',
},
];
const result = [];
if (
[InvoiceType.REFUND_CONFIRMATION, InvoiceType.REFUND_REQUEST].includes(
invoiceType,
)
) {
transaction.refund?.refund_items
?.filter((item) => Number(item.qty_refund) > 0)
.forEach((item) => {
const dataRow = [
{
text: item.transaction_item.item_name,
margin: [0, 5, 0, 5],
alignment: 'left',
},
{
text: item.qty_refund,
margin: [0, 5, 0, 5],
alignment: 'center',
},
{
text: Number(item.transaction_item.total_price).toLocaleString(
'id-ID',
{
style: 'currency',
currency: 'IDR',
},
),
margin: [0, 5, 0, 5],
alignment: 'right',
},
{
text: Number(item.refund_total).toLocaleString('id-ID', {
style: 'currency',
currency: 'IDR',
}),
alignment: 'right',
margin: [0, 5, 0, 5],
},
];
result.push(dataRow);
});
} else {
transaction.items.forEach((item) => {
const dataRow = [
{
text: item.item_name,
margin: [0, 5, 0, 5],
alignment: 'left',
},
{
text: item.qty,
margin: [0, 5, 0, 5],
alignment: 'center',
},
{
text: Number(item.item_price).toLocaleString('id-ID', {
style: 'currency',
currency: 'IDR',
}),
margin: [0, 5, 0, 5],
alignment: 'right',
},
{
text: Number(item.total_price).toLocaleString('id-ID', {
style: 'currency',
currency: 'IDR',
}),
alignment: 'right',
margin: [0, 5, 0, 5],
},
];
result.push(dataRow);
});
}
const body = [header, ...result];
return body;
}
export function mappingBody(
transaction: TransactionEntity,
invoiceType: InvoiceType,
) {
// booking date change information
if (invoiceType == InvoiceType.BOOKING_DATE_CHANGE) {
return [
"Great news! We've successfully updated your booking date as per your request. \n We're exited to accommodate your new plans and ensure everything goes smoothly \n\n",
'Here are your updated booking details:',
{
text: `\n\n Original Booking Date: ${new Date(
transaction['booking_date_before'],
).toDateString()} \n New Booking Date: ${new Date(
transaction.booking_date,
).toDateString()} \n\n`,
bold: true,
},
"Here's a quick recap of your order :",
];
}
// booking invoice
else if (invoiceType == InvoiceType.BOOKING_INVOICE) {
return [
"Thank you for choosing us! We're absolutely thrilled and can't wait to embark on this exiting day with you. See you soon for fun times ahead!",
];
} else if (invoiceType == InvoiceType.PAYMENT_CONFIRMATION) {
// booking payment confirmation
return [
'We are exited to inform you that your payment has been successfully received! \n',
'Attached to this email, you will find your confirmation receipt. \n',
'Please keep this safe as you will need to show it at the entrance upon your arrival. \n',
"It's your golden ticket to all the fun and excitement awaiting you \n\n",
"Here's a quick recap: \n",
];
}
// expired information invoice
// else if (invoiceType == InvoiceType.INVOICE_EXPIRED) {
// return [
// "We hope this message finds you well!",
// "Uh-oh! it looks like your invoice, dated 15 Juli, has officially expired as of 15 Juli. But no worries, we can fix this together \n",
// "To keep the goof times rolling, our friendly support team is just a call away at \n",
// "0564645 \n\n",
// "Here are the detail of the expired invoice: "
// ]
// }
// refund information
else if (invoiceType == InvoiceType.REFUND_REQUEST) {
return [
"We'ew trully sorry for any inconvenience that led to this request. \n",
"We've received your refund request for: \n",
];
}
// refund confirmation
else if (invoiceType == InvoiceType.REFUND_CONFIRMATION) {
return [
'Good news! \n',
"We've successfully processed your refund for: \n",
];
}
}
export function mappingBodyBottom(
transaction: TransactionEntity,
invoiceType: InvoiceType,
banks: PaymentMethodEntity[],
) {
if (invoiceType == InvoiceType.BOOKING_DATE_CHANGE) {
// booking date change information
return [
"For your convenience, we've attached a new confirmation receipt reflecting these changes \n",
'Please be sure to bring this updated receipt with you on the new date \n\n',
'If you have any questions or need further assistance, our friendly support team is just a call away at \n\n',
PhoneNumber,
"\nThank you and we can't wait to see you and make sure you hove an amazing time!",
];
} else if (invoiceType == InvoiceType.BOOKING_INVOICE) {
// booking invoice
return [
'Just a friendly reminder that your invoice will expire on 24 Agustus 2024 \n',
'To keep things running smoothly, please ensure your payment is completed before this data. \n',
'\nFor youe convenience, here is a list of our account details \n\n',
{
text: [
banks.forEach((bank) => {
return {
text: `${bank.issuer_name} ${bank.account_number} a/n ${bank.account_name} \n`,
bold: true,
};
}),
],
},
"\n Once you've made the payment, please kindly email or send the proof of payment so we can proceed with your booking promptly\n",
'If you have any questions or need assistance, feel free to reach out to our support team at\n',
PhoneNumber,
];
}
// booking payment confirmation
else if (invoiceType == InvoiceType.PAYMENT_CONFIRMATION) {
return [
'If you have any questions or need assistance, feel free to reach out to our support team at\n',
PhoneNumber,
"\nDon't forget to bring a smile and your confirmation receipt (attached) for a smooth entry. \n",
"We can't wait to see you and ensure you have an amazing time with us!",
];
}
// expired information invoice
else if (invoiceType == InvoiceType.INVOICE_EXPIRED) {
return [];
}
// refund information
else if (invoiceType == InvoiceType.REFUND_REQUEST) {
return [
"Your satisfaction is important to us, and we're commited to resolving this as quickly as possible.\n",
'Our team is already on it and will process your refund request promptly. \n',
"We'll keep you updated and notify you once the refund has been processed. \n",
"If you have any questions or need futher assistance, don't hestitate to reach out us at \n",
PhoneNumber,
'\n\n',
'Thank you for your patience and understanding \n',
'We appreciate your feedback and here to make things right!',
];
}
// refund confirmation
else if (invoiceType == InvoiceType.REFUND_CONFIRMATION) {
return [
'Here are the details of your refund: \n\n',
{
text: `Transaction Number: ${transaction.invoice_code} \n`,
},
{
text: `Refund Processed: ${transaction['refund'].refund_date}\n`,
},
{
text: `Bank Account: ${transaction['refund'].bank_name} \n`,
},
{
text: `Account Number: ${transaction['refund'].bank_account_number} \n`,
},
{
text: `Account Name: ${transaction['refund'].bank_account_name} \n`,
},
"\n We hope this helps make things righ, and we're he to assist if you need anything else. \n",
'You should see the refund reflected in your account within 3 Business days \n\n',
'Thank you for your patience and understanding\n',
'If you have any questions or need assistance, feel free to reach out to our support team at \n',
PhoneNumber,
"\nWe're alyways here to help!",
];
}
}

View File

@ -0,0 +1,215 @@
import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity';
import {
mappingBody,
mappingBodyBottom,
mappingFooter,
mappingHeader,
mappingItem,
mappingPrice,
} from './helpers/invoice-mapping.helper';
import { InvoiceType } from '../../constants';
import { PaymentMethodEntity } from 'src/modules/transaction/payment-method/domain/entities/payment-method.entity';
import * as fs from 'fs';
export function InvoiceTemplate(
transaction: TransactionEntity,
invoiceType: InvoiceType,
banks: PaymentMethodEntity[],
) {
const filePath = './assets/image/logo.jpeg';
const imageBase64 = fs.readFileSync(filePath).toString('base64');
const imageUrl = `data:image/png;base64,${imageBase64}`;
return {
content: [
{
alignment: 'justify',
columns: mappingHeader(transaction, invoiceType),
},
'\n\n',
// tipe booking date change tidak ada
{
columns: [
{
text: `Dear, \n ${transaction.customer_name} \n ${
transaction.customer_phone
} \n Booking Date: ${new Date(
transaction.booking_date,
).toDateString()}`,
bold: true,
},
],
},
'\n',
{
text: mappingBody(transaction, invoiceType),
},
'\n',
{
layout: {
defaultBorder: false,
hLineWidth: function () {
return 1;
},
vLineWidth: function () {
return 1;
},
hLineColor: function (i) {
return i === 0 ? '#000000' : '#eaeaea';
},
vLineColor: function () {
return '#eaeaea';
},
hLineStyle: function () {
// if (i === 0 || i === node.table.body.length) {
return null;
//}
},
// vLineStyle: function (i, node) { return {dash: { length: 10, space: 4 }}; },
paddingLeft: function () {
return 10;
},
paddingRight: function () {
return 10;
},
paddingTop: function () {
return 3;
},
paddingBottom: function () {
return 3;
},
fillColor: function () {
return '#fff';
},
},
table: {
widths: ['*', 'auto', 'auto', 100],
body: mappingItem(transaction, invoiceType),
},
},
'\n',
{
layout: {
defaultBorder: false,
hLineWidth: function () {
return 1;
},
vLineWidth: function () {
return 1;
},
hLineColor: function (i) {
return i === 3 ? '#000000' : '#eaeaea';
},
vLineColor: function () {
return '#eaeaea';
},
hLineStyle: function () {
// if (i === 0 || i === node.table.body.length) {
return null;
//}
},
// vLineStyle: function () { return {dash: { length: 10, space: 4 }}; },
paddingLeft: function () {
return 10;
},
paddingRight: function () {
return 10;
},
paddingTop: function () {
return 3;
},
paddingBottom: function (i) {
return i === 2 ? 20 : 3;
},
fillColor: function () {
return '#fff';
},
},
table: {
headerRows: 1,
widths: ['*', 100],
body: mappingPrice(transaction, invoiceType),
},
},
'\n',
{
text: mappingBodyBottom(transaction, invoiceType, banks),
},
'\n',
{
layout: {
defaultBorder: false,
hLineWidth: function () {
return 1;
},
vLineWidth: function () {
return 1;
},
hLineColor: function (i) {
return i === 0 ? '#000000' : '#eaeaea';
},
vLineColor: function () {
return '#eaeaea';
},
hLineStyle: function () {
// if (i === 0 || i === node.table.body.length) {
return null;
//}
},
// vLineStyle: function () { return {dash: { length: 10, space: 4 }}; },
paddingLeft: function () {
return 10;
},
paddingRight: function () {
return 10;
},
paddingTop: function () {
return 20;
},
paddingBottom: function () {
return 3;
},
fillColor: function () {
return '#fff';
},
},
table: {
widths: ['*', 200],
body: mappingFooter(),
},
},
],
styles: {
notesTitle: {
fontSize: 10,
bold: true,
margin: [0, 50, 0, 3],
},
notesText: {
fontSize: 10,
},
tableExample: {
margin: [0, 5, 0, 15],
headerRows: 1,
widths: ['*', 100],
},
tableHeader: {
bold: true,
fontSize: 13,
color: 'black',
},
},
defaultStyle: {
columnGap: 20,
},
images: {
data: imageUrl,
},
};
}

View File

@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PdfMakeManager } from './domain/managers/pdf-make.manager';
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
import { PaymentMethodDataService } from 'src/modules/transaction/payment-method/data/services/payment-method-data.service';
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
import { TransactionItemModel } from 'src/modules/transaction/transaction/data/models/transaction-item.model';
import { PaymentMethodModel } from 'src/modules/transaction/payment-method/data/models/payment-method.model';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
import { CqrsModule } from '@nestjs/cqrs';
import { TransactionTaxModel } from 'src/modules/transaction/transaction/data/models/transaction-tax.model';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forFeature([TransactionModel], CONNECTION_NAME.DEFAULT),
CqrsModule,
],
controllers: [],
providers: [PdfMakeManager],
})
export class ExportModule {}

View File

@ -0,0 +1,16 @@
import { Controller, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Public } from 'src/core/guards';
@ApiTags('export')
@Controller('export')
@Public(true)
export class ExportController {
constructor() {}
// @Post('pdf/example')
// async exportPdf(
// ): Promise<any> {
// return PdfMaker();
// }
}

View File

@ -0,0 +1,77 @@
import { google } from 'googleapis';
import * as fs from 'fs';
import { TransactionEntity } from 'src/modules/transaction/transaction/domain/entities/transaction.entity';
export async function CreateEventCalendarHelper(
transaction: TransactionEntity,
isDelete = false,
) {
let result;
const filePath = './assets/json/google-credential.json';
const credential = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const client = new google.auth.JWT({
email: credential.client_email,
key: credential.private_key,
scopes: ['https://www.googleapis.com/auth/calendar'],
});
const calendar = google.calendar({
version: 'v3',
auth: client,
});
const eventData = mappingData(transaction);
if (transaction.calendar_id) {
result = await calendar.events.update(
{
calendarId: process.env.GOOGLE_CALENDAR_ID,
eventId: transaction.calendar_id,
requestBody: eventData,
},
{},
);
} else if (!isDelete) {
result = await calendar.events.insert(
{
calendarId: process.env.GOOGLE_CALENDAR_ID,
requestBody: eventData,
},
{},
);
} else {
result = await calendar.events.delete(
{
calendarId: process.env.GOOGLE_CALENDAR_ID,
eventId: transaction.calendar_id,
},
{},
);
}
return result?.data;
}
function mappingData(transaction) {
return {
summary: transaction.customer_name ?? transaction.invoice_code,
description: `<b>Booking for invoice ${
transaction.invoice_code
}</b><p>List Items :</p><ul>${transaction.items.map(
(item) => `<li>${item.item_name}</li>`,
)}</ul>`,
start: {
dateTime: new Date(transaction.booking_date).toISOString(),
timeZone: 'Asia/Jakarta',
},
end: {
dateTime: new Date(transaction.booking_date).toISOString(),
timeZone: 'Asia/Jakarta',
},
reminders: {
useDefault: false,
},
};
}

View File

@ -8,7 +8,7 @@ export class IndexHolidayCalendarManager {
const events = [];
const calendar = google.calendar({
version: 'v3',
auth: 'AIzaSyCsCt6PDd6uYLkahvtdvCoMWf-1_QaLiNM',
auth: process.env.GOOGLE_CALENDAR_KEY,
});
const calendarId = 'id.indonesian#holiday@group.v.calendar.google.com';

View File

@ -4,10 +4,29 @@ import { CqrsModule } from '@nestjs/cqrs';
import { IndexHolidayCalendarManager } from '../../configuration/google-calendar/domain/usecases/managers/index-holiday-google-calendar.manager';
import { GoogleCalendarController } from './infrastructure/google-calendar.controller';
import { GoogleCalendarOrchestrator } from './domain/usecases/google-calendar.orchestrator';
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
import { TransactionItemModel } from 'src/modules/transaction/transaction/data/models/transaction-item.model';
import { TransactionTaxModel } from 'src/modules/transaction/transaction/data/models/transaction-tax.model';
@Module({
imports: [ConfigModule.forRoot(), CqrsModule],
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forFeature(
[TransactionModel, TransactionItemModel, TransactionTaxModel],
CONNECTION_NAME.DEFAULT,
),
CqrsModule,
],
controllers: [GoogleCalendarController],
providers: [IndexHolidayCalendarManager, GoogleCalendarOrchestrator],
providers: [
IndexHolidayCalendarManager,
TransactionDataService,
GoogleCalendarOrchestrator,
],
})
export class GoogleCalendarModule {}

View File

@ -1,5 +1,5 @@
import { GoogleCalendarOrchestrator } from './../domain/usecases/google-calendar.orchestrator';
import { Controller, Get, Query } from '@nestjs/common';
import { Controller, Get, Post, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Public } from 'src/core/guards';
import { FilterGoogleCalendarDto } from './dto/filter-google-calendar.dto';

View File

@ -0,0 +1,34 @@
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
import { Column, Entity } from 'typeorm';
import { PosLogEntity, PosLogType } from '../../domain/entities/pos-log.entity';
import { BaseCoreModel } from 'src/core/modules/data/model/base-core.model';
@Entity(TABLE_NAME.LOG_POS)
export class PosLogModel
extends BaseCoreModel<PosLogEntity>
implements PosLogEntity
{
@Column('varchar', { name: 'type', default: PosLogType.cash_witdrawal })
type: PosLogType;
@Column('bigint', { name: 'pos_number', nullable: true })
pos_number: number;
@Column('decimal', { name: 'total_balance', nullable: true })
total_balance: number;
@Column('bigint', { name: 'created_at', nullable: true })
created_at: number;
@Column('varchar', { name: 'creator_name', nullable: true })
creator_name: string;
@Column('varchar', { name: 'creator_id', nullable: true })
creator_id: string;
@Column('varchar', { name: 'drawn_by_name', nullable: true })
drawn_by_name: string;
@Column('varchar', { name: 'drawn_by_id', nullable: true })
drawn_by_id: string;
}

View File

@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { BaseDataService } from 'src/core/modules/data/service/base-data.service';
import { PosLogEntity } from '../../domain/entities/pos-log.entity';
import { PosLogModel } from '../models/pos-log.model';
import { InjectRepository } from '@nestjs/typeorm';
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
import { Repository } from 'typeorm';
@Injectable()
export class PosLogService extends BaseDataService<PosLogEntity> {
constructor(
@InjectRepository(PosLogModel, CONNECTION_NAME.DEFAULT)
private repo: Repository<PosLogModel>,
) {
super(repo);
}
}

View File

@ -0,0 +1,17 @@
import { BaseCoreEntity } from 'src/core/modules/domain/entities/base-core.entity';
export interface PosLogEntity extends BaseCoreEntity {
type: PosLogType;
pos_number: number;
total_balance: number;
created_at: number;
creator_name: string;
creator_id: string;
}
export enum PosLogType {
cash_witdrawal = 'cash withdrawal',
opening_cash = 'opening_cash',
login = 'login',
logout = 'logout',
}

View File

@ -1,9 +1,33 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { RecordLog } from '../entities/log.event';
import { LogService } from '../../data/services/log.service';
import { LogModel } from '../../data/models/log.model';
@EventsHandler(RecordLog)
export class RecordLogHandler implements IEventHandler<RecordLog> {
constructor(private dataService: LogService) {}
async handle(event: RecordLog) {
// TODO: Implement logic here
const data = event.data;
const queryRunner = this.dataService
.getRepository()
.manager.connection.createQueryRunner();
const log = new LogModel();
Object.assign(log, {
data_id: data.id,
module: data.module,
description: data.description,
process: data.op,
old_data: data.old,
data: data.data,
created_at: new Date().getTime(),
creator_name: data.user.name,
creator_id: data.user.id,
});
await this.dataService.create(queryRunner, LogModel, log);
}
}

View File

@ -0,0 +1,41 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { ChangeDocEvent } from 'src/modules/configuration/couch/domain/events/change-doc.event';
import { PosLogService } from '../../data/services/pos-log.service';
import { PosLogModel } from '../../data/models/pos-log.model';
import { PosLogType } from '../entities/pos-log.entity';
@EventsHandler(ChangeDocEvent)
export class RecordPosLogHandler implements IEventHandler<ChangeDocEvent> {
constructor(private dataService: PosLogService) {}
async handle(event: ChangeDocEvent) {
try {
const database = event.data.database;
const data = event.data.data;
if (!['pos_cash_activity', 'pos_activity'].includes(database)) return;
const queryRunner = this.dataService
.getRepository()
.manager.connection.createQueryRunner();
const activity = new PosLogModel();
Object.assign(activity, {
id: data._id,
type: PosLogType[data.type],
total_balance: data.withdrawal_cash ?? data.opening_cash_balance,
pos_number: data.pos_number,
creator_id: data.pos_admin?.id,
creator_name: data.pos_admin?.name ?? data.pos_admin?.username,
drawn_by_id: data.withdraw_user?.id,
drawn_by_name: data.withdraw_user?.name ?? data.withdraw_user?.username,
created_at: data.created_at,
});
await this.dataService.create(queryRunner, PosLogModel, activity);
} catch (error) {
console.log('error handling pos activity couch');
}
}
}

View File

@ -9,12 +9,15 @@ import { RecordErrorLogHandler } from './domain/handlers/error-log.handler';
import { RecordLogHandler } from './domain/handlers/log.handler';
import { ErrorLogService } from './data/services/error-log.service';
import { LogService } from './data/services/log.service';
import { PosLogModel } from './data/models/pos-log.model';
import { PosLogService } from './data/services/pos-log.service';
import { RecordPosLogHandler } from './domain/handlers/pos-log.handler';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forFeature(
[LogModel, ErrorLogModel],
[LogModel, ErrorLogModel, PosLogModel],
CONNECTION_NAME.DEFAULT,
),
CqrsModule,
@ -22,9 +25,11 @@ import { LogService } from './data/services/log.service';
controllers: [],
providers: [
RecordLogHandler,
RecordPosLogHandler,
RecordErrorLogHandler,
LogService,
PosLogService,
ErrorLogService,
],
})

View File

@ -4,9 +4,20 @@ import { PaymentMethodDataService } from 'src/modules/transaction/payment-method
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
import { TransactionChangeStatusEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-change-status.event';
import { sendEmail } from '../helpers/send-email.helper';
import { TransactionPaymentType } from 'src/modules/transaction/transaction/constants';
import { InvoiceType } from 'src/modules/configuration/export/constants';
import { GeneratePdf } from 'src/modules/configuration/export/domain/templates/helpers/generate-pdf.helper';
import { TransactionUpdatedEvent } from 'src/modules/transaction/transaction/domain/entities/event/transaction-updated.event';
import { RefundChangeStatusEvent } from 'src/modules/transaction/refund/domain/entities/event/refund-change-status.event';
import { RefundCreatedEvent } from 'src/modules/transaction/refund/domain/entities/event/refund-created.event';
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
import { Logger } from '@nestjs/common';
@EventsHandler(TransactionChangeStatusEvent)
@EventsHandler(
TransactionChangeStatusEvent,
TransactionUpdatedEvent,
RefundChangeStatusEvent,
RefundCreatedEvent,
)
export class PaymentTransactionHandler
implements IEventHandler<TransactionChangeStatusEvent>
{
@ -16,46 +27,272 @@ export class PaymentTransactionHandler
) {}
async handle(event: TransactionChangeStatusEvent) {
const data_id = event.data.id;
const old_data = event.data.old;
const current_data = event.data.data;
let payments = [];
try {
const old_data = event.data.old;
const current_data = event.data.data;
const data_id = current_data.transaction_id ?? event.data.id;
const from_refund = event.data.module == TABLE_NAME.REFUND;
console.log('payment handlet', { data_id });
if (
old_data.status == STATUS.DRAFT &&
current_data.status == STATUS.PENDING &&
current_data.payment_type != TransactionPaymentType.COUNTER &&
!!current_data.customer_email
) {
if (current_data.payment_type != TransactionPaymentType.MIDTRANS) {
payments = await this.paymentService.getManyByOptions({
where: {
status: STATUS.ACTIVE,
},
});
}
const payments = await this.paymentService.getManyByOptions({
where: {
status: STATUS.ACTIVE,
},
});
const transaction = await this.dataService.getOneByOptions({
let transaction = await this.dataService.getOneByOptions({
where: {
id: data_id,
},
relations: ['items'],
relations: [
'items',
'refunds',
'refunds.refund_items',
'refunds.refund_items.transaction_item',
],
});
try {
if (!transaction) {
transaction = await this.dataService.getOneByOptions({
where: {
id: event.data.id,
},
relations: [
'items',
'refunds',
'refunds.refund_items',
'refunds.refund_items.transaction_item',
],
});
}
Object.assign(transaction, {
booking_date: new Date(transaction.booking_date).toDateString(),
booking_date_before: new Date(
transaction.booking_date_before,
).toDateString(),
email: transaction.customer_email,
payment_methods: payments,
});
const refund = transaction?.['refunds']?.find(
(refund) => ![STATUS.CANCEL].includes(refund.status),
);
if (refund) {
Object.assign(refund, {
refund_date: new Date(refund?.refund_date).toDateString(),
request_date: new Date(refund?.request_date).toDateString(),
refund_total: Number(refund?.refund_total).toLocaleString('id-ID', {
style: 'currency',
currency: 'IDR',
}),
});
Object.assign(transaction, {
refund: refund,
});
}
if (transaction?.['refund']?.refund_items.length > 0) {
Object.assign(transaction, {
refund_items: `
<p>Refund Items:</p>
<ul style="padding-left:15px">
${transaction?.['refund']?.refund_items
?.filter((item) => Number(item.qty_refund) > 0)
.map((item) => {
return `
<li>${item.qty_refund} ${item.transaction_item.item_name} </li>
`;
})}
</ul>`,
});
}
if (!transaction.customer_email) return;
// refund request
if (
from_refund &&
transaction['refund'] &&
[STATUS.DRAFT].includes(transaction['refund'].status)
) {
Logger.verbose('Send Refund Request', 'PaymentTransaction');
const pdf = await GeneratePdf(
transaction,
InvoiceType.REFUND_REQUEST,
payments,
);
sendEmail(
[
{
...transaction,
email: transaction.customer_email,
payment_methods: payments,
payment_total: Number(transaction.payment_total).toLocaleString(
'id-ID',
{
style: 'currency',
currency: 'IDR',
},
),
},
],
'Payment Confirmation',
InvoiceType.REFUND_REQUEST,
pdf,
);
} catch (error) {
console.log(error);
}
// refund confirmation
else if (
from_refund &&
transaction['refund'] &&
transaction['refund'].status == STATUS.REFUNDED
) {
Logger.verbose('Send Refund Confirmation', 'PaymentTransaction');
const pdf = await GeneratePdf(
transaction,
InvoiceType.REFUND_CONFIRMATION,
payments,
);
sendEmail(
[
{
...transaction,
payment_total: Number(transaction.payment_total).toLocaleString(
'id-ID',
{
style: 'currency',
currency: 'IDR',
},
),
},
],
InvoiceType.REFUND_CONFIRMATION,
pdf,
);
}
// payment settled
else if (
!from_refund &&
old_data.status != current_data.status &&
[STATUS.ACTIVE, STATUS.SETTLED].includes(current_data.status)
) {
Logger.verbose('Send Payment Settled', 'PaymentTransaction');
const pdf = await GeneratePdf(
transaction,
InvoiceType.PAYMENT_CONFIRMATION,
payments,
);
sendEmail(
[
{
...transaction,
payment_total: Number(transaction.payment_total).toLocaleString(
'id-ID',
{
style: 'currency',
currency: 'IDR',
},
),
},
],
InvoiceType.PAYMENT_CONFIRMATION,
pdf,
);
}
// payment confirm to pending
else if (
!from_refund &&
old_data.status != current_data.status &&
[STATUS.PENDING].includes(current_data.status)
) {
Logger.verbose('Send Confirmation to Pending', 'PaymentTransaction');
const pdf = await GeneratePdf(
transaction,
InvoiceType.BOOKING_INVOICE,
payments,
);
sendEmail(
[
{
...transaction,
payment_total: Number(transaction.payment_total).toLocaleString(
'id-ID',
{
style: 'currency',
currency: 'IDR',
},
),
},
],
InvoiceType.BOOKING_INVOICE,
pdf,
);
}
// payment expired
else if (
!from_refund &&
old_data.status != current_data.status &&
[STATUS.PENDING, STATUS.EXPIRED].includes(current_data.status)
) {
Logger.verbose('Send Payment Expired', 'PaymentTransaction');
const pdf = await GeneratePdf(
transaction,
InvoiceType.INVOICE_EXPIRED,
payments,
);
sendEmail(
[
{
...transaction,
payment_total: Number(transaction.payment_total).toLocaleString(
'id-ID',
{
style: 'currency',
currency: 'IDR',
},
),
},
],
InvoiceType.INVOICE_EXPIRED,
pdf,
);
}
// change booking date
else if (
!from_refund &&
old_data.booking_date != current_data.booking_date &&
[STATUS.SETTLED, STATUS.ACTIVE, STATUS.PENDING].includes(
current_data.status,
)
) {
Logger.verbose('Send Change Booking Date', 'PaymentTransaction');
const pdf = await GeneratePdf(
transaction,
InvoiceType.BOOKING_DATE_CHANGE,
payments,
);
sendEmail(
[
{
...transaction,
payment_total: Number(transaction.payment_total).toLocaleString(
'id-ID',
{
style: 'currency',
currency: 'IDR',
},
),
},
],
InvoiceType.BOOKING_DATE_CHANGE,
pdf,
);
}
} catch (error) {
console.log(error);
}
}
}

View File

@ -1,10 +1,10 @@
import * as nodemailer from 'nodemailer';
import * as handlebars from 'handlebars';
import * as path from 'path';
import * as fs from 'fs';
import { TransactionPaymentType } from 'src/modules/transaction/transaction/constants';
import { InvoiceType } from 'src/modules/configuration/export/constants';
export async function sendEmail(receivers, subject) {
export async function sendEmail(receivers, invoiceType, attachment?) {
const smtpTransport = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_POST,
@ -13,38 +13,62 @@ export async function sendEmail(receivers, subject) {
pass: process.env.EMAIL_TOKEN,
},
});
let templateName = 'payment-confirmation-bank';
for (const receiver of receivers) {
if (receiver.payment_type == TransactionPaymentType.MIDTRANS)
templateName = 'payment-confirmation-midtrans';
try {
const templateName = getTemplate(receiver.payment_type, invoiceType);
const templatePath = `./assets/email-template/${templateName}.html`;
const templateSource = fs.readFileSync(templatePath, 'utf8');
let templatePath = path.resolve(
__dirname,
`../email-template/${templateName}.html`,
);
templatePath = templatePath.replace(/dist/g, 'src');
const templateSource = fs.readFileSync(templatePath, 'utf8');
const template = handlebars.compile(templateSource);
const htmlToSend = template(receiver);
const template = handlebars.compile(templateSource);
const htmlToSend = template(receiver);
const emailContext = {
from: process.env.EMAIL_SENDER ?? 'no-reply@weplayground.app',
to: receiver.email,
subject: invoiceType,
html: htmlToSend,
attachDataUrls: true,
attachments: [
{
filename: `${invoiceType}.pdf`,
content: attachment,
},
],
};
const emailContext = {
from: 'no-reply@eigen.co.id',
to: receiver.email,
subject: subject,
html: htmlToSend,
attachDataUrls: true,
};
await new Promise((f) => setTimeout(f, 2000));
await new Promise((f) => setTimeout(f, 2000));
smtpTransport.sendMail(emailContext, function (err, data) {
if (err) {
console.log(`Error occurs on send to ${receiver.email}`);
} else {
console.log(`Email sent to ${receiver.email}`);
}
});
smtpTransport.sendMail(emailContext, function (err) {
if (err) {
console.log(err, `Error occurs on send to ${receiver.email}`);
} else {
console.log(`Email sent to ${receiver.email}`);
}
});
} catch (error) {
console.log(error, `Error occurs on send to ${receiver.email}`);
}
}
}
function getTemplate(transactionType, invoiceType) {
if (invoiceType == InvoiceType.BOOKING_DATE_CHANGE) {
return 'change-date-information';
} else if (invoiceType == InvoiceType.INVOICE_EXPIRED) {
return 'invoice-expired';
} else if (invoiceType == InvoiceType.REFUND_REQUEST) {
return 'refund-request';
} else if (invoiceType == InvoiceType.REFUND_CONFIRMATION) {
return 'refund-confirmation';
} else if (invoiceType == InvoiceType.PAYMENT_CONFIRMATION) {
return 'payment-confirmation';
} else if (
invoiceType == InvoiceType.BOOKING_INVOICE &&
transactionType != TransactionPaymentType.MIDTRANS
) {
return 'invoice-bank';
} else if (invoiceType == InvoiceType.BOOKING_INVOICE) {
return 'invoice-midtrans';
}
}

View File

@ -0,0 +1,55 @@
import { Controller, Get, Injectable } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Public } from 'src/core/guards';
import * as path from 'path';
import * as fs from 'fs';
@ApiTags(`email templates`)
@Controller('v1/email-templates')
@Public()
@Injectable()
export class MailTemplateController {
getTemplate(templateName) {
const templatePath = path.join(
__dirname,
'../../../../../',
`src/modules/configuration/mail/domain/email-template/${templateName}.html`,
);
return fs.readFileSync(templatePath, 'utf8');
}
@Get('date-change')
async getDateChange() {
return this.getTemplate('change-date');
}
@Get('invoice')
async getBookingInvoice() {
return this.getTemplate('invoice');
}
@Get('payment-confirmation-bank')
async getPaymentConfirmation() {
return this.getTemplate('payment-confirmation-bank');
}
@Get('payment-confirmation-midtrans')
async getPaymentConfirmationMidtrans() {
return this.getTemplate('payment-confirmation-midtrans');
}
@Get('invoice-expired')
async getInvoiceExpired() {
return this.getTemplate('invoice-expired');
}
@Get('refund-confirmation')
async getRefundConfirmation() {
return this.getTemplate('refund-confirmation');
}
@Get('refund-request')
async getRefundRequest() {
return this.getTemplate('refund-request');
}
}

View File

@ -8,6 +8,8 @@ import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
import { TransactionDataService } from 'src/modules/transaction/transaction/data/services/transaction-data.service';
import { PaymentTransactionHandler } from './domain/handlers/payment-transaction.handler';
import { MailTemplateController } from './infrastructure/mail.controller';
import { PdfMakeManager } from '../export/domain/managers/pdf-make.manager';
@Module({
imports: [
@ -18,8 +20,10 @@ import { PaymentTransactionHandler } from './domain/handlers/payment-transaction
),
CqrsModule,
],
controllers: [],
controllers: [MailTemplateController],
providers: [
PdfMakeManager,
PaymentTransactionHandler,
PaymentMethodDataService,
TransactionDataService,

View File

@ -1,14 +1,28 @@
import { Injectable } from '@nestjs/common';
import { EventBus } from '@nestjs/cqrs';
import { mappingMidtransTransaction } from '../../domain/usecases/helpers/mapping-transaction.helper';
const midtransClient = require('midtrans-client');
import { Snap } from 'midtrans-client';
import { MidtransStatus } from '../../domain/entities/midtrans-callback.event';
import { TransactionReadService } from 'src/modules/transaction/transaction/data/services/transaction-read.service';
import * as moment from 'moment';
@Injectable()
export class MidtransService {
constructor(private eventBus: EventBus) {}
constructor(private transaction: TransactionReadService) {}
isMoreThan24HoursAgo(dateString) {
const date = moment(dateString, 'YYYY-MM-DD', true);
if (!date.isValid()) {
return false;
}
const now = moment();
const diffInHours = now.diff(date, 'hours');
return diffInHours > 24;
}
get midtransInstance() {
return new midtransClient.Snap({
return new Snap({
isProduction: false,
serverKey: process.env.MIDTRANS_SERVER_KEY,
clientKey: process.env.MIDTRANS_CLIENT_KEY,
@ -19,6 +33,32 @@ export class MidtransService {
return await this.midtransInstance.transaction.status(orderId);
}
async syncPendingStatus(): Promise<any[]> {
const pendingIds = await this.transaction.getPendingOrderId();
const responses = [];
for (const transaction of pendingIds) {
const { id, invoice_date } = transaction;
let status;
try {
status = await this.getStatus(id);
} catch (error) {
status = {
order_id: id,
transaction_status: this.isMoreThan24HoursAgo(invoice_date)
? 'cancel'
: 'pending',
};
}
responses.push(status);
}
return responses;
}
async changeStatus(orderId: string, action: MidtransStatus): Promise<any> {
return await this.midtransInstance.transaction[action](orderId);
}
async create(body): Promise<any> {
const data = mappingMidtransTransaction(body);
return await this.midtransInstance.createTransaction(data);

View File

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

View File

@ -2,7 +2,7 @@ export function mappingMidtransTransaction(transaction) {
const item_details = transaction.items?.map((item) => {
return {
quantity: Number(item.qty),
price: Number(item.total_price),
price: Number(item.total_price) / Number(item.qty),
name: item.item_name,
category: item.item_category_name,
};

View File

@ -1,9 +1,21 @@
import { Body, Controller, Get, Injectable, Param, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import {
Body,
Controller,
Get,
Injectable,
Param,
Post,
Query,
UnprocessableEntityException,
} from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger';
import { Public } from 'src/core/guards';
import { MidtransService } from '../data/services/midtrans.service';
import { EventBus } from '@nestjs/cqrs';
import { MidtransCallbackEvent } from '../domain/entities/midtrans-callback.event';
import {
MidtransCallbackEvent,
MidtransStatus,
} from '../domain/entities/midtrans-callback.event';
import { MidtransDto } from './dto/midtrans.dto';
@ApiTags(`midtrans`)
@ -18,7 +30,58 @@ export class MidtransController {
@Get(':id/status')
async getStatus(@Param('id') id: string) {
return await this.dataService.getStatus(id);
try {
const data = await this.dataService.getStatus(id);
this.eventBus.publishAll([
new MidtransCallbackEvent({
id: id,
data: data,
}),
]);
return 'Berhasil update status transaksi';
} catch (error) {
console.log(error.message);
throw new Error('Gagal update status transaksi');
}
}
@Get('sync')
async syncStatus() {
try {
const results = await this.dataService.syncPendingStatus();
for (const data of results) {
this.eventBus.publishAll([
new MidtransCallbackEvent({
id: data.order_id,
data: data,
}),
]);
}
return 'Berhasil update status transaksi';
} catch (error) {
console.log(error.message);
throw new Error('Gagal update status transaksi');
}
}
@Get(':id/change-status')
@ApiQuery({ name: 'status', enum: MidtransStatus })
async cancel(
@Param('id') id: string,
@Query('status') status = MidtransStatus.cancel,
) {
try {
return await this.dataService.changeStatus(id, status);
} catch (error) {
const data =
error.ApiResponse?.status_message ??
error.message ??
'Gagal update status transaksi';
throw new UnprocessableEntityException(data);
}
}
@Post('callback')

View File

@ -3,12 +3,20 @@ import { CqrsModule } from '@nestjs/cqrs';
import { MidtransController } from './infrastructure/midtrans.controller';
import { MidtransService } from './data/services/midtrans.service';
import { Global, Module } from '@nestjs/common';
import { TransactionReadService } from 'src/modules/transaction/transaction/data/services/transaction-read.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
import { TransactionModel } from 'src/modules/transaction/transaction/data/models/transaction.model';
@Global()
@Module({
imports: [ConfigModule.forRoot(), CqrsModule],
imports: [
ConfigModule.forRoot(),
CqrsModule,
TypeOrmModule.forFeature([TransactionModel], CONNECTION_NAME.DEFAULT),
],
controllers: [MidtransController],
providers: [MidtransService],
providers: [MidtransService, TransactionReadService],
exports: [MidtransService],
})
export class MidtransModule {}

View File

@ -24,7 +24,13 @@ export class BatchDeleteItemCategoryManager extends BaseBatchDeleteManager<ItemC
}
get validateRelations(): validateRelations[] {
return [{ relation: 'items' }];
return [
{
relation: 'items',
message:
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
},
];
}
get entityTarget(): any {

View File

@ -24,7 +24,13 @@ export class BatchInactiveItemCategoryManager extends BaseBatchUpdateStatusManag
}
get validateRelations(): validateRelations[] {
return [{ relation: 'items' }];
return [
{
relation: 'items',
message:
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
},
];
}
get entityTarget(): any {

View File

@ -27,7 +27,13 @@ export class DeleteItemCategoryManager extends BaseDeleteManager<ItemCategoryEnt
}
get validateRelations(): validateRelations[] {
return [{ relation: 'items' }];
return [
{
relation: 'items',
message:
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
},
];
}
get entityTarget(): any {

View File

@ -27,7 +27,13 @@ export class InactiveItemCategoryManager extends BaseUpdateStatusManager<ItemCat
}
get validateRelations(): validateRelations[] {
return [{ relation: 'items' }];
return [
{
relation: 'items',
message:
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
},
];
}
get entityTarget(): any {

View File

@ -27,7 +27,17 @@ export class UpdateItemCategoryManager extends BaseUpdateManager<ItemCategoryEnt
}
get validateRelations(): validateRelations[] {
return [];
if (this.data.item_type != this.oldData.item_type) {
return [
{
relation: 'items',
message:
'Gagal! tidak dapat mengubah tipe item karena sudah berelasi dengan item',
},
];
} else {
return [];
}
}
get uniqueColumns(): columnUniques[] {
@ -42,7 +52,6 @@ export class UpdateItemCategoryManager extends BaseUpdateManager<ItemCategoryEnt
return [
{
topic: ItemCategoryUpdatedEvent,
data: this.data,
},
];
}

View File

@ -1,7 +1,9 @@
import { BaseFilterEntity } from 'src/core/modules/domain/entities/base-filter.entity';
import { ItemType } from 'src/modules/item-related/item-category/constants';
export interface FilterItemRateEntity extends BaseFilterEntity {
item_ids: string[];
item_types: ItemType[];
season_period_ids: string[];
start_date: Date;
end_date: Date;

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