diff options
| author | Rafi Zadanly <zadanlyr@gmail.com> | 2023-03-03 16:44:12 +0700 |
|---|---|---|
| committer | Rafi Zadanly <zadanlyr@gmail.com> | 2023-03-03 16:44:12 +0700 |
| commit | 76961c8312312609dbef0646274f6dd1f6c2bf19 (patch) | |
| tree | 111c9ff63449f4e188a72435a850ac8efc2a9d28 | |
| parent | 069f9fa637cd24e9b92c7a1e4de56fa9e263508f (diff) | |
add midtrans payment email notification
| -rw-r--r-- | src/lib/checkout/components/Checkout.jsx | 6 | ||||
| -rw-r--r-- | src/lib/checkout/components/FinishCheckout.jsx | 18 | ||||
| -rw-r--r-- | src/lib/checkout/email/FinishCheckoutEmail.jsx | 326 | ||||
| -rw-r--r-- | src/pages/api/shop/finish-checkout.js | 91 | ||||
| -rw-r--r-- | src/pages/api/shop/midtrans-payment.js | 33 | ||||
| -rw-r--r-- | src/pages/shop/checkout/finish.jsx | 2 |
6 files changed, 459 insertions, 17 deletions
diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 42608cef..8af3d996 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -126,7 +126,7 @@ const Checkout = () => { `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}` ) for (const product of products) deleteItemCart({ productId: product.id }) - window.snap.pay(payment.data.token) + window.location.href = payment.data.redirectUrl } return ( @@ -252,7 +252,7 @@ const Checkout = () => { <button className='flex-1 btn-yellow' onClick={checkout} - disabled={isLoading} + disabled={isLoading || !products || products?.length == 0} > {isLoading ? 'Loading...' : 'Bayar'} </button> @@ -261,7 +261,7 @@ const Checkout = () => { <Script async src='https://app.sandbox.midtrans.com/snap/snap.js' - data-client-key='' + data-client-key={process.env.NEXT_PUBLIC_MIDTRANS_CLIENT_KEY} /> </> ) diff --git a/src/lib/checkout/components/FinishCheckout.jsx b/src/lib/checkout/components/FinishCheckout.jsx index a7d65dd0..f5346d67 100644 --- a/src/lib/checkout/components/FinishCheckout.jsx +++ b/src/lib/checkout/components/FinishCheckout.jsx @@ -1,8 +1,16 @@ import Link from '@/core/components/elements/Link/Link' -import useTransaction from '@/lib/transaction/hooks/useTransaction' +import axios from 'axios' +import { useEffect } from 'react' -const FinishCheckout = ({ id }) => { - const { transaction } = useTransaction({ id }) +const FinishCheckout = ({ query }) => { + useEffect(() => { + if (query?.order_id) { + console.log(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/finish-checkout?orderName=${query.order_id}`); + axios.post( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/finish-checkout?orderId=${query.order_id}` + ) + } + }, [query]) return ( <div className='p-4'> @@ -13,11 +21,11 @@ const FinishCheckout = ({ id }) => { Rincian belanja sudah kami kirimkan ke email anda. Mohon dicek kembali. jika tidak menerima email, anda dapat menghubungi kami disini. </p> - <p className='mb-2 font-medium'>{transaction.data?.name}</p> + <p className='mb-2 font-medium'>{query?.order_id?.replaceAll('-', '/')}</p> <p className='text-caption-2 text-yellow_r-11'>No. Transaksi</p> </div> <Link - href={transaction.data?.id ? `/my/transaction/${transaction.data.id}` : '/'} + href='/my/transactions' className='bg-yellow_r-6 text-yellow_r-12 rounded-b-xl py-4 block' > Lihat detail pembelian Anda disini diff --git a/src/lib/checkout/email/FinishCheckoutEmail.jsx b/src/lib/checkout/email/FinishCheckoutEmail.jsx new file mode 100644 index 00000000..950fe318 --- /dev/null +++ b/src/lib/checkout/email/FinishCheckoutEmail.jsx @@ -0,0 +1,326 @@ +import currencyFormat from '@/core/utils/currencyFormat' +import toTitleCase from '@/core/utils/toTitleCase' +import { + Body, + Column, + Container, + Head, + Heading, + Hr, + Html, + Img, + Row, + Section, + Text +} from '@react-email/components' + +const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => { + return ( + <Html> + <Head /> + <Body style={style.main}> + <Container style={style.container}> + <Section style={{ marginTop: '32px' }}> + <Img + src={`${process.env.SELF_HOST}/images/indoteknik-logo.png`} + width='150' + height='50' + alt='Indoteknik.com' + style={{ margin: '0 auto' }} + /> + </Section> + <Heading style={style.h1}> + {statusPayment == 'success' && 'Terimakasih untuk pembelian anda!'} + {statusPayment == 'pending' && 'Menunggu Pembayaran'} + {statusPayment == 'failed' && 'Pembayaran Tidak Berhasil'} + </Heading> + + <Text style={style.text}>Hai {transaction.address.customer.name},</Text> + <Text style={style.text}> + {statusPayment == 'success' && ( + <> + Terima kasih atas kepercayaan anda berbelanja di Indoteknik. Dengan ini kami + informasikan transaksi yang anda lakukan sebesar{' '} + <span style={{ color: '#E20613' }}>{currencyFormat(payment.grossAmount)}</span>{' '} + sudah berhasil dilakukan. Pembelian anda akan segera kami proses dan kirim sesuai + dengan antrian pesanan yang masuk. + </> + )} + {statusPayment == 'pending' && ( + <> + Terima kasih atas kepercayaan anda berbelanja di Indoteknik. Dengan ini kami + informasikan transaksi yang anda lakukan sebesar{' '} + <span style={{ color: '#E20613' }}>{currencyFormat(payment.grossAmount)}</span>{' '} + belum dilakukan. Silahkan lakukan pembayaran pada hari ini sebelum stoknya + kehabisan. + </> + )} + {statusPayment == 'failed' && ( + <> + Terima kasih atas kepercayaan anda berbelanja di Indoteknik. Dengan ini kami + informasikan transaksi yang anda lakukan Tidak Berhasil. Mohon untuk tidak melakukan + pembayaran dikarenakan transaksi ini gagal kami terima. Segera lakukan pembelian + kembali dengan produk yang anda inginkan di website Indoteknik.com. + </> + )} + </Text> + <Text style={style.text}> + {['pending', 'failed'].includes(statusPayment) && ( + <> + Jika anda mengalami kesulitan, dapat menghubungi Customer Service kami untuk + menanyakan transaksi anda lakukan melalui Whatsapp kami. + </> + )} + {statusPayment == 'success' && ( + <> + Anda dapat menghubungi Customer Service kami untuk menanyakan status pesanan yang + sudah berhasil anda lakukan melalui Whatsapp kami. + </> + )} + </Text> + + <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}> + <strong>Detail Transaksi</strong> + </Text> + + <Hr style={style.hr} /> + + <Section style={style.alert}> + {statusPayment == 'success' && + 'Struk ini dapat anda simpan sebagai bukti tambahan dalam transaksi yang telah dilakukan.'} + {statusPayment == 'pending' && + 'Kami akan menginformasikan melalui email setelah anda berhasil melakukan pembayaran.'} + {statusPayment == 'failed' && + 'Dimohon untuk tidak melakukan pembayaran. Karena transaksi anda tidak berhasil dibuat.'} + </Section> + + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>No Transaksi (SO)</Column> + <Column style={style.descriptionRCol}>{transaction.name}</Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Tanggal Transaksi</Column> + <Column style={style.descriptionRCol}>{payment.transactionTime}</Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Status Pembayaran</Column> + <Column style={{ ...style.descriptionRCol }}> + {statusPayment == 'success' && ( + <div style={{ ...style.badge, ...style.badgeGreen }}>Berhasil</div> + )} + {statusPayment == 'pending' && ( + <div style={{ ...style.badge, ...style.badgeRed }}>Pending</div> + )} + {statusPayment == 'failed' && ( + <div style={{ ...style.badge, ...style.badgeRed }}>Tidak Berhasil</div> + )} + </Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Metode Pembayaran</Column> + <Column style={style.descriptionRCol}> + {toTitleCase(payment.paymentType.replaceAll('_', ' '))} + </Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Batas Akhir Pembayaran</Column> + <Column style={style.descriptionRCol}>{payment.expiryTime}</Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Nominal Transfer</Column> + <Column style={style.descriptionRCol}> + <span style={{ fontWeight: '600' }}>{currencyFormat(payment.grossAmount)}</span> + </Column> + </Row> + + <Text style={{ ...style.text, lineHeight: '100%', marginTop: '24px' }}> + <strong>Detail Produk</strong> + </Text> + + <Hr style={style.hr} /> + + {transaction.products.map((product) => ( + <Row + style={style.productRow} + key={product.id} + > + <Column style={style.productLCol}> + <Img + src={product.parent.image} + width='100%' + /> + </Column> + <Column style={style.productRCol}> + <Text style={style.productName}>{product.name}</Text> + <Text style={style.productCode}>{product.code}</Text> + <div style={{ dislay: 'flex' }}> + <span style={style.productPriceA}> + {currencyFormat(product.price.priceDiscount)} + </span> + {product.price.discountPercentage > 0 && ( + <> + + <span style={style.productPriceB}>{currencyFormat(product.price.price)}</span> + </> + )} + x {product.quantity} barang + </div> + </Column> + </Row> + ))} + + <Hr style={style.hr} /> + + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Subtotal</Column> + <Column style={style.descriptionRCol}>{currencyFormat(transaction.subtotal)}</Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Total Diskon</Column> + <Column style={{ ...style.descriptionRCol, color: '#E20613' }}> + {currencyFormat(transaction.discountTotal)} + </Column> + </Row> + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>PPN 11% (Incl.)</Column> + <Column style={style.descriptionRCol}> + {currencyFormat(transaction.subtotal * 0.11)} + </Column> + </Row> + + <Hr style={style.hr} /> + + <Row style={style.descriptionRow}> + <Column style={style.descriptionLCol}>Grand Total</Column> + <Column style={style.descriptionRCol}> + <span style={{ fontWeight: '600' }}>{currencyFormat(transaction.amountTotal)}</span> + </Column> + </Row> + + <Hr style={style.hr} /> + + <Text style={{ ...style.text, margin: '12px 0 3px' }}>Best regards,</Text> + + <Text style={{ ...style.text, margin: '3px 0 0' }}> + <strong>PT. Indoteknik Dotcom Gemilang</strong> + <br /> + Jl. Bandengan Utara 85A No. 8-9 Penjaringan. + <br /> + Kec. Penjaringan, Jakarta Utara - DKI Jakarta + </Text> + </Container> + </Body> + </Html> + ) +} + +const style = { + main: { + backgroundColor: '#ffffff', + margin: '0 auto', + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif" + }, + container: { + border: '1px solid #eaeaea', + borderRadius: '5px', + margin: '40px auto', + padding: '20px', + width: '465px' + }, + h1: { + color: '#000', + fontSize: '24px', + fontWeight: 'normal', + textAlign: 'center', + margin: '30px 0', + padding: '0' + }, + text: { + color: '#000', + fontSize: '14px', + lineHeight: '24px' + }, + alert: { + backgroundColor: '#FDF1C7', + border: '1px solid #F8C20A', + padding: '8px', + borderRadius: '6px', + fontSize: '14px', + margin: '16px 0' + }, + hr: { + border: 'none', + borderTop: '1px solid #eaeaea', + margin: '8px 0', + width: '100%' + }, + descriptionRow: { + width: '100%', + margin: '10px 0', + fontSize: '14px' + }, + descriptionLCol: { + width: '50%', + color: '#6B7280' + }, + descriptionRCol: { + width: '50%', + textAlign: 'right' + }, + productRow: { + width: '100%', + margin: '10px 0', + fontSize: '14px' + }, + productLCol: { + width: '25%', + border: '1px solid #eaeaea', + borderRadius: '4px', + padding: '1px' + }, + productRCol: { + width: '75%', + padding: '0 8px', + verticalAlign: 'top' + }, + productName: { + lineHeight: '121%', + margin: '0 0 8px' + }, + productCode: { + color: '#6B7280', + lineHeight: '100%', + margin: '0 0 8px', + fontSize: '14px' + }, + productPriceA: { + lineHeight: '100%', + fontSize: '14px' + }, + productPriceB: { + color: '#6B7280', + lineHeight: '100%', + fontSize: '14px', + textDecoration: 'line-through' + }, + badge: { + padding: '3px 6px', + borderRadius: '6px', + fontSize: '14px', + width: 'fit-content', + marginLeft: 'auto' + }, + badgeRed: { + color: '#E20613', + backgroundColor: '#FCE2E4', + border: '1px solid #E20613' + }, + badgeGreen: { + color: '#16A34A', + backgroundColor: '#E7F4E9', + border: '1px solid #16A34A' + } +} + +export default FinishCheckoutEmail diff --git a/src/pages/api/shop/finish-checkout.js b/src/pages/api/shop/finish-checkout.js new file mode 100644 index 00000000..66874549 --- /dev/null +++ b/src/pages/api/shop/finish-checkout.js @@ -0,0 +1,91 @@ +import odooApi from '@/core/api/odooApi' +import mailer from '@/core/utils/mailer' +import FinishCheckoutEmail from '@/lib/checkout/email/FinishCheckoutEmail' +import { render } from '@react-email/render' +import axios from 'axios' +import camelcaseObjectDeep from 'camelcase-object-deep' + +export default async function handler(req, res) { + const { orderName = null } = req.query + + if (!orderName) { + return res.status(422).json({ error: 'parameter missing' }) + } + + let { auth } = req.cookies + + if (!auth) { + return res.status(401).json({ error: 'Unauthorized' }) + } + auth = JSON.parse(auth) + + const midtransAuthKey = btoa(process.env.MIDTRANS_SERVER_KEY + ':') + const midtransHeaders = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Basic ${midtransAuthKey}` + } + let midtransStatus = await axios.get(`${process.env.MIDTRANS_HOST}/v2/${orderName}/status`, { + headers: midtransHeaders + }) + midtransStatus = camelcaseObjectDeep(midtransStatus.data) + if (!midtransStatus?.orderId) { + return res.status(400).json({ error: 'Payment Not Found' }) + } + + const query = `name=${orderName.replaceAll('-', '/')}&limit=1` + const searchTransaction = await odooApi( + 'GET', + `/api/v1/partner/${auth.partnerId}/sale_order?${query}`, + {}, + { Token: auth.token } + ) + if (searchTransaction.saleOrderTotal == 0) { + return res.status(400).json({ error: 'Transaction Not Found' }) + } + + let transaction = await odooApi( + 'GET', + `/api/v1/partner/${auth.partnerId}/sale_order/${searchTransaction.saleOrders[0].id}`, + {}, + { Token: auth.token } + ) + if (!transaction?.id) { + return res.status(400).json({ error: 'Transaction Detail Not Found' }) + } + + transaction.subtotal = 0 + transaction.discountTotal = 0 + for (const product of transaction.products) { + transaction.subtotal += product.price.price * product.quantity + transaction.discountTotal -= + (product.price.price - product.price.priceDiscount) * product.quantity + } + + let statusPayment = '' + const transactionStatus = midtransStatus.transactionStatus + if (['capture', 'settlement'].includes(transactionStatus)) { + statusPayment = 'success' + } else if (transactionStatus == 'pending') { + statusPayment = 'pending' + } else { + statusPayment = 'failed' + } + + const emailMessage = render( + <FinishCheckoutEmail + transaction={transaction} + payment={midtransStatus} + statusPayment={statusPayment} + /> + ) + + mailer.sendMail({ + from: 'sales@indoteknik.com', + to: transaction.address.customer.email, + subject: 'Pembelian di Indoteknik.com', + html: emailMessage + }) + + return res.status(200).json({ description: 'success' }) +} diff --git a/src/pages/api/shop/midtrans-payment.js b/src/pages/api/shop/midtrans-payment.js index a9bf16ac..be676d38 100644 --- a/src/pages/api/shop/midtrans-payment.js +++ b/src/pages/api/shop/midtrans-payment.js @@ -1,17 +1,18 @@ import odooApi from '@/core/api/odooApi' +import camelcaseObjectDeep from 'camelcase-object-deep' import midtransClient from 'midtrans-client' export default async function handler(req, res) { const { transactionId = null } = req.query if (!transactionId) { - res.status(422).json({ error: 'parameter missing' }) + return res.status(422).json({ error: 'parameter missing' }) } let { auth } = req.cookies if (!auth) { - res.status(401).json({ error: 'Unauthorized' }) + return res.status(401).json({ error: 'Unauthorized' }) } auth = JSON.parse(auth) @@ -22,7 +23,7 @@ export default async function handler(req, res) { { Token: auth.token } ) if (!transaction?.id) { - res.status(400).json({ error: 'No Data' }) + return res.status(400).json({ error: 'No Data' }) } const snap = new midtransClient.Snap({ @@ -32,20 +33,36 @@ export default async function handler(req, res) { const parameter = { transaction_details: { - order_id: transaction.name, + order_id: transaction.name?.replaceAll('/', '-'), gross_amount: transaction.amountTotal }, credit_card: { secure: true }, + item_details: transaction.products.map((product) => ({ + id: product.code, + price: Math.round(product.price.priceDiscount), + quantity: product.quantity, + name: product.name?.substring(0, 50) + })), customer_details: { - first_name: transaction.address.invoice.name, - email: transaction.address.invoice.email, - phone: transaction.address.invoice.phone + first_name: transaction.address.customer.name, + email: transaction.address.customer.email || '', + phone: transaction.address.customer.phone || '', + billing_address: { + first_name: transaction.address.invoice.name, + email: transaction.address.invoice.email || '', + phone: transaction.address.invoice.phone || '' + }, + shipping_address: { + first_name: transaction.address.shipping.name, + email: transaction.address.shipping.email || '', + phone: transaction.address.shipping.phone || '' + } } } const midtransTransaction = await snap.createTransaction(parameter) - res.status(200).json(midtransTransaction) + return res.status(200).json(camelcaseObjectDeep(midtransTransaction)) } diff --git a/src/pages/shop/checkout/finish.jsx b/src/pages/shop/checkout/finish.jsx index cc64199f..eb7631a0 100644 --- a/src/pages/shop/checkout/finish.jsx +++ b/src/pages/shop/checkout/finish.jsx @@ -9,7 +9,7 @@ export default function Finish() { return ( <IsAuth> <BasicLayout> - <FinishCheckoutComponent id={router.query.id || 0} /> + <FinishCheckoutComponent query={router.query || {}} /> </BasicLayout> </IsAuth> ) |
