├── .gitignore ├── src ├── dbconfig.js ├── init-modes.js ├── db.js ├── commonfunc.js ├── index.js ├── payment_PaymentService_model.js └── paymentService.js ├── package.json └── DDL.sql /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/dbconfig.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require("sequelize"); 2 | const sequelize = new Sequelize( 3 | process.env.DB_NAME, 4 | process.env.DB_USERNAME, 5 | process.env.DB_PASSWORD, 6 | { 7 | host: process.env.DB_HOST, 8 | port: process.env.DB_PORT || 3306, 9 | dialect: "mysql", 10 | 11 | } 12 | ); 13 | 14 | 15 | 16 | module.exports = sequelize; -------------------------------------------------------------------------------- /src/init-modes.js: -------------------------------------------------------------------------------- 1 | var _payment_PaymentService = require("./payment_PaymentService_model"); 2 | 3 | function initModels(sequelize) { 4 | var payment_PaymentService = _payment_PaymentService(sequelize, DataTypes); 5 | 6 | return { 7 | payment_PaymentService, 8 | }; 9 | } 10 | module.exports.initModels = initModels; 11 | module.exports.default = initModels; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interview_job", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node src/index.js", 8 | "dev": "ts-node-dev src/index.ts" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "dependencies": { 15 | "express": "^4.21.2", 16 | "ts-node-dev": "^2.0.0" 17 | }, 18 | "devDependencies": { 19 | "@types/express": "^5.0.0", 20 | "typescript": "^5.7.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | const ErrorLogSQL = { 2 | insert: `INSERT INTO payment_error_log ( 3 | user_id, 4 | change_date, 5 | change_type, 6 | data, 7 | audit_id 8 | ) VALUES (?,NOW(),?,?, ?); 9 | `, 10 | init: `CREATE TABLE IF NOT EXISTS payment_error_log ( 11 | log_id int AUTO_INCREMENT PRIMARY KEY, 12 | user_id int NOT NULL, 13 | change_date datetime(0) NOT NULL, 14 | change_type text NOT NULL, 15 | audit_id int NOT NULL, 16 | data LONGTEXT 17 | );` 18 | }; 19 | 20 | module.exports = ErrorLogSQL; -------------------------------------------------------------------------------- /src/commonfunc.js: -------------------------------------------------------------------------------- 1 | const errorsql = require("./errorlogsql"); 2 | const pool = require("./dbconfig"); 3 | 4 | function executeQuery(query, params) { 5 | return new Promise((resolve, reject) => { 6 | pool.getConnection((err, connection) => { 7 | if (err) { 8 | reject(err); 9 | return; 10 | } 11 | connection.query(query, params, (error, results) => { 12 | connection.release(); 13 | if (error) { 14 | reject(error); 15 | return; 16 | } 17 | resolve(results); 18 | }); 19 | }); 20 | }); 21 | } 22 | 23 | const logError = async (user_id, error_type, error_data, auditId) => { 24 | try { 25 | await executeQuery(errorsql.insert, [user_id, error_type, error_data, auditId]); 26 | } catch (error) { 27 | console.error("Failed to log error:", error); 28 | } 29 | }; 30 | 31 | module.exports = { logError, executeQuery }; 32 | -------------------------------------------------------------------------------- /DDL.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `payment_PaymentService` ( 2 | `id` int NOT NULL AUTO_INCREMENT, 3 | `status` varchar(45) DEFAULT NULL, 4 | `transactionId` varchar(45) DEFAULT NULL, 5 | `transactionDate` varchar(45) DEFAULT NULL, 6 | `orderId` varchar(45) DEFAULT NULL, 7 | `paymentType` int DEFAULT NULL, 8 | `orderAmount` decimal(24,8) NOT NULL, 9 | `orderDetail` varchar(45) DEFAULT NULL, 10 | `UID` varchar(45) DEFAULT NULL, 11 | `paymentMode` varchar(250) DEFAULT NULL, 12 | `memberName` varchar(250) DEFAULT NULL, 13 | `memberEmail` varchar(45) DEFAULT NULL, 14 | `memberphone` varchar(45) DEFAULT NULL, 15 | `paymentCallbackByServiceURL` varchar(250) DEFAULT NULL, 16 | `expiredDtm` datetime DEFAULT NULL, 17 | `clientIP` varchar(255) DEFAULT NULL, 18 | `response_param` text, 19 | `currency` varchar(255) DEFAULT NULL, 20 | `createdDtm` datetime NOT NULL, 21 | `updatedDtm` datetime NOT NULL, 22 | `countryId` int DEFAULT NULL, 23 | `merchantBranchId` int DEFAULT NULL, 24 | PRIMARY KEY (`id`), 25 | UNIQUE KEY `id_UNIQUE` (`id`) USING BTREE, 26 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const cookieParser = require("cookie-parser"); 3 | const express = require("express"); 4 | 5 | const { initStripe, queryStripeOrder, stripeCallback, StripeStatusCheck } = require("./paymentService"); 6 | 7 | const app = express(); 8 | const port = 5000; 9 | 10 | app.use(express.json()); 11 | app.use(express.urlencoded()); 12 | app.use(cookieParser()); 13 | 14 | app.use(async function (req, res, next) { 15 | const login_secret = req?.cookies?.token; // or req.body.login_secret if it's in the request body 16 | if (!login_secret) { 17 | return res.status(401).send({ 18 | code: 401, 19 | message: "No login Token Provided", 20 | }); 21 | } 22 | next(); 23 | }); 24 | 25 | cron.schedule("*/5 * * * *", StripeStatusCheck); 26 | //create stripe order 27 | app.post("/api/topup", multipart(), initStripe); 28 | 29 | // query stripe order 30 | app.get("/api/topup/:tx_id", queryStripeOrder); 31 | 32 | // callback url for stripe 33 | app.post("/api/stripe/callback", multipart(), stripeCallback); 34 | 35 | app.listen(port, () => { 36 | console.log(`Server is running on port ${port}`); 37 | }); 38 | -------------------------------------------------------------------------------- /src/payment_PaymentService_model.js: -------------------------------------------------------------------------------- 1 | module.exports = function (sequelize, DataTypes) { 2 | return sequelize.define( 3 | "payment_PaymentService", 4 | { 5 | id: { 6 | autoIncrement: true, 7 | type: DataTypes.INTEGER, 8 | allowNull: false, 9 | primaryKey: true, 10 | }, 11 | status: { 12 | type: DataTypes.STRING(45), 13 | allowNull: true, 14 | }, 15 | transactionId: { 16 | type: DataTypes.STRING(45), 17 | allowNull: true, 18 | }, 19 | transactionDate: { 20 | type: DataTypes.STRING(45), 21 | allowNull: true, 22 | }, 23 | orderId: { 24 | type: DataTypes.STRING(45), 25 | allowNull: true, 26 | }, 27 | mixPaymentId: { 28 | type: DataTypes.INTEGER, 29 | allowNull: true, 30 | }, 31 | paymentType: { 32 | type: DataTypes.INTEGER, 33 | allowNull: true, 34 | references: { 35 | model: "payment_PaymentMethodConfig", 36 | key: "id", 37 | }, 38 | }, 39 | orderAmount: { 40 | type: DataTypes.DECIMAL(24, 8), 41 | allowNull: false, 42 | }, 43 | orderDetail: { 44 | type: DataTypes.STRING(45), 45 | allowNull: true, 46 | }, 47 | UID: { 48 | type: DataTypes.STRING(45), 49 | allowNull: true, 50 | }, 51 | paymentMode: { 52 | type: DataTypes.STRING(250), 53 | allowNull: true, 54 | }, 55 | memberName: { 56 | type: DataTypes.STRING(250), 57 | allowNull: true, 58 | }, 59 | memberEmail: { 60 | type: DataTypes.STRING(45), 61 | allowNull: true, 62 | }, 63 | memberphone: { 64 | type: DataTypes.STRING(45), 65 | allowNull: true, 66 | }, 67 | paymentCallbackByServiceURL: { 68 | type: DataTypes.STRING(250), 69 | allowNull: true, 70 | }, 71 | expiredDtm: { 72 | type: DataTypes.DATE, 73 | allowNull: true, 74 | }, 75 | clientIP: { 76 | type: DataTypes.STRING, 77 | allowNull: true, 78 | }, 79 | response_param: { 80 | type: DataTypes.TEXT, 81 | allowNull: true, 82 | }, 83 | currency: { 84 | type: DataTypes.STRING, 85 | allowNull: true, 86 | }, 87 | countryId: { 88 | type: DataTypes.INTEGER, 89 | allowNull: true, 90 | }, 91 | merchantBranchId: { 92 | type: DataTypes.INTEGER, 93 | allowNull: true, 94 | }, 95 | }, 96 | { 97 | sequelize, 98 | tableName: "payment_PaymentService", 99 | createdAt: "createdDtm", 100 | updatedAt: "updatedDtm", 101 | indexes: [ 102 | { 103 | name: "PRIMARY", 104 | unique: true, 105 | using: "BTREE", 106 | fields: [{ name: "id" }], 107 | }, 108 | { 109 | name: "id_UNIQUE", 110 | unique: true, 111 | using: "BTREE", 112 | fields: [{ name: "id" }], 113 | }, 114 | { 115 | name: "payment_PaymentService_payment_PaymentMethodConfig_FK", 116 | using: "BTREE", 117 | fields: [{ name: "paymentType" }], 118 | }, 119 | ], 120 | } 121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /src/paymentService.js: -------------------------------------------------------------------------------- 1 | const sequelize = require("./db"); 2 | var initModels = require("./init-models"); 3 | const models = initModels(sequelize); 4 | const Stripe = require("stripe"); 5 | 6 | const getExpiredTime = "SELECT value from payment_parameters where code = 'expiredMinutes'"; 7 | const selectPaymentServiceByOrderId = "SELECT orderId from payment_PaymentService where orderId =?"; 8 | 9 | const initStripe = async (req, res, next) => { 10 | let { 11 | orderId, 12 | orderAmount, 13 | orderDetail, 14 | UID, 15 | memberName, 16 | memberEmail, 17 | memberPhone, 18 | paymentCallbackByServiceURL, 19 | isInstallment, 20 | mixPaymentId, 21 | currency, 22 | merchantBranchId, 23 | countryId, 24 | } = req.body; 25 | let expiredMinutesCall = executeQuery(getExpiredTime); 26 | let isRepayCall = executeQuery(selectPaymentServiceByOrderId, [orderId, mixPaymentId ?? 0]); 27 | let [expiredMinutes, isRepayCallRes] = await Promise.all([expiredMinutesCall, isRepayCall]); 28 | let isRepay = isRepayCallRes?.length > 0; 29 | let insertPaymentServiceRes; 30 | if (!isRepay) { 31 | // create new order 32 | insertPaymentServiceRes = await models.payment_PaymentService.create({ 33 | orderId, 34 | paymentType: 7, 35 | status: "init", 36 | orderAmount, // no need conversion 37 | orderDetail, 38 | UID, 39 | memberName, 40 | memberEmail, 41 | memberphone: memberPhone, 42 | paymentCallbackByServiceURL, 43 | mixPaymentId: mixPaymentId ?? 0, 44 | expiredDtm: sequelize.literal(`DATE_ADD(NOW(), INTERVAL ${expiredMinutes?.[0]?.value} MINUTE)`), 45 | currency, 46 | merchantBranchId, 47 | countryId, 48 | }); 49 | } else { 50 | insertPaymentServiceRes = await models.payment_PaymentService.findOne({ 51 | where: { 52 | orderId, 53 | }, 54 | }); 55 | } 56 | 57 | // query key 58 | const paymentConfig = await models.payment_cust_paymentMethod_config.findOne({ 59 | where: { 60 | paymentType: 7, // stripe 61 | [Op.or]: [{ merchantBranchId }, { merchantBranchId: 0 }], 62 | [Op.or]: [{ countryId }, { countryId: 0 }], 63 | }, 64 | order: [ 65 | ["merchantBranchId", "DESC"], // Prefer records with variableMerchantBranchId if both exist 66 | ], 67 | }); 68 | 69 | let paymentIntent; 70 | try { 71 | // api doc = https://docs.stripe.com/api/payment_intents/create 72 | const stripe = new Stripe(paymentConfig.merchantSecret); 73 | 74 | const paymentIntentParam = { 75 | amount: orderAmount, 76 | currency: currency, 77 | automatic_payment_methods: { 78 | enabled: true, 79 | }, 80 | // confirm: true, // Set to true to attempt to confirm this PaymentIntent immediately. This parameter defaults to false. When creating and confirming a PaymentIntent at the same time, you can also provide the parameters available in the Confirm API. 81 | // customer: '', // D of the Customer this PaymentIntent belongs to, if one exists. Payment methods attached to other Customers cannot be used with this PaymentIntent. If setup_future_usage is set and this PaymentIntent’s payment method is not card_present, then the payment method attaches to the Customer after the PaymentIntent has been confirmed and any required actions from the user are complete. If the payment method is card_present and isn’t a digital wallet, then a generated_card payment method representing the card is created and attached to the Customer instead. 82 | description: orderDetail, // An arbitrary string attached to the object. Often useful for displaying to users. 83 | // metadata: {}, // Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format. Individual keys can be unset by posting an empty value to them. All keys can be unset by posting an empty value to metadata. 84 | // off_session: "", // boolean | string, only when confirm=true. Set to true to indicate that the customer isn’t in your checkout flow during this payment attempt and can’t authenticate. Use this parameter in scenarios where you collect card details and charge them later. This parameter can only be used with confirm=true. 85 | // payment_method: "", // ID of the payment method (a PaymentMethod, Card, or compatible Source object) to attach to this PaymentIntent. If you omit this parameter with confirm=true, customer.default_source attaches as this PaymentIntent’s payment instrument to improve migration for users of the Charges API. We recommend that you explicitly provide the payment_method moving forward. 86 | // receipt_email: "test@gmail.com", // Email address to send the receipt to. If you specify receipt_email for a payment in live mode, you send a receipt regardless of your email settings. 87 | // setup_future_usage: "", // Indicates that you intend to make future payments with this PaymentIntent’s payment method. If you provide a Customer with the PaymentIntent, you can use this parameter to attach the payment method to the Customer after the PaymentIntent is confirmed and the customer completes any required actions. If you don’t provide a Customer, you can still attach the payment method to a Customer after the transaction completes. If the payment method is card_present and isn’t a digital wallet, Stripe creates and attaches a generated_card payment method representing the card to the Customer instead. When processing card payments, Stripe uses setup_future_usage to help you comply with regional legislation and network rules, such as SCA. 88 | // setup_future_usage desc // off_session = Use off_session if your customer may or may not be present in your checkout flow. 89 | // setup_future_usage desc // on_session = Use on_session if you intend to only reuse the payment method when your customer is present in your checkout flow. 90 | // shipping: { 91 | // address: { 92 | // city: "", 93 | // country: "MY", 94 | // line1: "", 95 | // line2: "", 96 | // postal_code: "", 97 | // state: "", 98 | // }, 99 | // name: "", // Required, Recipient name. 100 | // carrier: "", // The delivery service that shipped a physical product, such as Fedex, UPS, USPS, etc. 101 | // phone: "", // Recipient phone (including extension). 102 | // tracking_number: "", // The tracking number for a physical product, obtained from the delivery service. If multiple tracking numbers were generated for this purchase, please separate them with commas. 103 | // }, 104 | // statement_descriptor: "", // Text that appears on the customer’s statement as the statement descriptor for a non-card charge. This value overrides the account’s default statement descriptor. For information about requirements, including the 22-character limit, see the Statement Descriptor docs. Setting this value for a card charge returns an error. For card charges, set the statement_descriptor_suffix instead. 105 | // statement_descriptor_suffix: "", // Provides information about a card charge. Concatenated to the account’s statement descriptor prefix to form the complete statement descriptor that appears on the customer’s statement. 106 | // application_fee_amount: 0, // The amount of the application fee (if any) that will be requested to be applied to the payment and transferred to the application owner’s Stripe account. The amount of the application fee collected will be capped at the total payment amount. For more information, see the PaymentIntents use case for connected accounts. 107 | // capture_method: "", // Controls when the funds will be captured from the customer’s account. 108 | // capture_method desc // automatic = Stripe automatically captures funds when the customer authorizes the payment. 109 | // capture_method desc // automatic_async = (Default) Stripe asynchronously captures funds when the customer authorizes the payment. Recommended over capture_method=automatic due to improved latency. Read the integration guide for more information. 110 | // capture_method desc // manual = Place a hold on the funds when the customer authorizes the payment, but don’t capture the funds until later. (Not all payment methods support this.) 111 | // confirmation_method: "", // Describes whether we can confirm this PaymentIntent automatically, or if it requires customer action to confirm the payment. 112 | // confirmation_method desc // automatic = (Default) PaymentIntent can be confirmed using a publishable key. After next_actions are handled, no additional confirmation is required to complete the payment. 113 | // confirmation_method desc // manual = All payment attempts must be made using a secret key. The PaymentIntent returns to the requires_confirmation state after handling next_actions, and requires your server to initiate each payment attempt with an explicit confirmation. 114 | // confirmation_token: "", // only when confirm=true, ID of the ConfirmationToken used to confirm this PaymentIntent. If the provided ConfirmationToken contains properties that are also being provided in this request, such as payment_method, then the values in this request will take precedence. 115 | // error_on_requires_action: "", // only when confirm=true, Set to true to fail the payment attempt if the PaymentIntent transitions into requires_action. Use this parameter for simpler integrations that don’t handle customer actions, such as saving cards without authentication. This parameter can only be used with confirm=true. 116 | // mandate: "", // only when confirm=true, ID of the mandate that’s used for this payment. This parameter can only be used with confirm=true. 117 | // mandate_data: { 118 | // customer_acceptance: { 119 | // type: "", // The type of customer acceptance information included with the Mandate. One of online or offline. 120 | // accepted_at: "", // timestamp, secret key only. The time at which the customer accepted the Mandate. 121 | // offline: {}, // object. If this is a Mandate accepted offline, this hash contains details about the offline acceptance. 122 | // online: { 123 | // ip_address: "", // Required. The IP address from which the Mandate was accepted by the customer. 124 | // user_agent: "", // Required. The user agent of the browser from which the Mandate was accepted by the customer. 125 | // }, // object. If this is a Mandate accepted online, this hash contains details about the online acceptance. 126 | // }, // This hash contains details about the customer acceptance of the Mandate. 127 | // }, // only when confirm=true, This hash contains details about the Mandate to create. This parameter can only be used with confirm=true. 128 | // on_behalf_of: "", // Connect only. The Stripe account ID that these funds are intended for. Learn more about the use case for connected accounts. 129 | // payment_method_configuration: "", // The ID of the payment method configuration to use with this PaymentIntent. 130 | // payment_method_data: { 131 | // type: "", // enum, Required. The type of the PaymentMethod. An additional hash is included on the PaymentMethod with a name matching this value. It contains additional information specific to the PaymentMethod type. 132 | // acss_debit: { 133 | // account_number: "", // Required. Customer’s bank account number. 134 | // institution_number: "", // Required. Institution number of the customer’s bank. 135 | // transit_number: "", // Required. Transit number of the customer’s bank. 136 | // }, // If this is an acss_debit PaymentMethod, this hash contains details about the ACSS Debit payment method. 137 | // affirm: {}, // object. If this is an affirm PaymentMethod, this hash contains details about the Affirm payment method. 138 | // afterpay_clearpay: {}, // object.If this is an AfterpayClearpay PaymentMethod, this hash contains details about the AfterpayClearpay payment method. 139 | // alipay: {}, // object.If this is an Alipay PaymentMethod, this hash contains details about the Alipay payment method. 140 | // allow_redisplay: "", //This field indicates whether this payment method can be shown again to its customer in a checkout flow. Stripe products such as Checkout and Elements use this field to determine whether a payment method can be shown as a saved payment method in a checkout flow. The field defaults to unspecified. 141 | // // allow_redisplay desc // always = Use always to indicate that this payment method can always be shown to a customer in a checkout flow. 142 | // // allow_redisplay desc // limited = Use limited to indicate that this payment method can’t always be shown to a customer in a checkout flow. For example, it can only be shown in the context of a specific subscription. 143 | // // allow_redisplay desc // unspecified = This is the default value for payment methods where allow_redisplay wasn’t set. 144 | // alma: {}, // object. If this is a Alma PaymentMethod, this hash contains details about the Alma payment method. 145 | // amazon_pay: {}, // object. If this is a AmazonPay PaymentMethod, this hash contains details about the AmazonPay payment method. 146 | // au_becs_debit: { 147 | // account_number: "", // Required. The account number for the bank account. 148 | // bsb_number: "", // Required. Bank-State-Branch number of the bank account. 149 | // }, // object. If this is an au_becs_debit PaymentMethod, this hash contains details about the bank account. 150 | // bacs_debit: { 151 | // account_number: "", // Account number of the bank account that the funds will be debited from. 152 | // sort_code: "", // Sort code of the bank account. (e.g., 10-20-30) 153 | // }, // object. If this is a bacs_debit PaymentMethod, this hash contains details about the Bacs Direct Debit bank account. 154 | // bancontact: {}, // If this is a bancontact PaymentMethod, this hash contains details about the Bancontact payment method. 155 | // billing_details: { 156 | // address: { 157 | // city: "", 158 | // country: "", 159 | // line1: "", 160 | // line2: "", 161 | // postal_code, 162 | // state: "", 163 | // }, // Billing address. 164 | // email: "", // Email address. 165 | // name: "", // Full name. 166 | // phone: "", // Billing phone number (including extension). 167 | // }, // Billing information associated with the PaymentMethod that may be used or required by particular types of payment methods. 168 | // blik: {}, // If this is a blik PaymentMethod, this hash contains details about the BLIK payment method. 169 | // boleto: { 170 | // tax_id: "", // Required. The tax ID of the customer (CPF for individual consumers or CNPJ for businesses consumers) 171 | // }, // If this is a boleto PaymentMethod, this hash contains details about the Boleto payment method. 172 | // cashapp: {}, // If this is a cashapp PaymentMethod, this hash contains details about the Cash App Pay payment method. 173 | // customer_balance: {}, // If this is a customer_balance PaymentMethod, this hash contains details about the CustomerBalance payment method. 174 | // eps: { 175 | // bank: "", // The customer’s bank. 176 | // }, // If this is an eps PaymentMethod, this hash contains details about the EPS payment method. 177 | // fpx: { 178 | // bank: "", // Required. The customer’s bank. 179 | // }, // If this is an fpx PaymentMethod, this hash contains details about the FPX payment method. 180 | // giropay: {}, // If this is a giropay PaymentMethod, this hash contains details about the Giropay payment method. 181 | // grabpay: {}, // If this is a grabpay PaymentMethod, this hash contains details about the GrabPay payment method. 182 | // ideal: { 183 | // bank: "", // The customer’s bank. Only use this parameter for existing customers. Don’t use it for new customers. 184 | // }, // If this is an ideal PaymentMethod, this hash contains details about the iDEAL payment method. 185 | // interac_present: {}, // If this is an interac_present PaymentMethod, this hash contains details about the Interac Present payment method. 186 | // kakao_pay: {}, // If this is a kakao_pay PaymentMethod, this hash contains details about the Kakao Pay payment method. 187 | // klarna: { 188 | // dob: { 189 | // day: 1, // integer. Required. The day of birth, between 1 and 31. 190 | // month: 1, // integer. Required. The month of birth, between 1 and 12. 191 | // year: 1234, // integer. Required. The four-digit year of birth. 192 | // }, // Customer’s date of birth 193 | // }, // If this is a klarna PaymentMethod, this hash contains details about the Klarna payment method. 194 | // konbini: {}, // If this is a konbini PaymentMethod, this hash contains details about the Konbini payment method. 195 | // kr_card: {}, // If this is a kr_card PaymentMethod, this hash contains details about the Korean Card payment method. 196 | // link: {}, // If this is an Link PaymentMethod, this hash contains details about the Link payment method. 197 | // metadata: {}, // object. Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format. Individual keys can be unset by posting an empty value to them. All keys can be unset by posting an empty value to metadata. 198 | // mobilepay: {}, // If this is a mobilepay PaymentMethod, this hash contains details about the MobilePay payment method. 199 | // multibanco: {}, // If this is a multibanco PaymentMethod, this hash contains details about the Multibanco payment method. 200 | // naver_pay: { 201 | // funding: "", // enum. Whether to use Naver Pay points or a card to fund this transaction. If not provided, this defaults to card. 202 | // // funding desc // card = Use a card to fund this transaction. 203 | // // funding desc // points = Use Naver Pay points to fund this transaction. 204 | // }, // If this is a naver_pay PaymentMethod, this hash contains details about the Naver Pay payment method. 205 | // oxxo: {}, // If this is an oxxo PaymentMethod, this hash contains details about the OXXO payment method. 206 | // p24: { 207 | // bank:'',// enum. The customer’s bank. 208 | // },// If this is a p24 PaymentMethod, this hash contains details about the P24 payment method. 209 | 210 | // }, // If provided, this hash will be used to create a PaymentMethod. The new PaymentMethod will appear in the payment_method property on the PaymentIntent. 211 | // return_url: "", // The URL to redirect your customer back to after they authenticate or cancel their payment on the payment method’s app or site. If you’d prefer to redirect to a mobile application, you can alternatively supply an application URI scheme. This parameter can only be used with confirm=true. 212 | use_stripe_sdk: true, // Set to true when confirming server-side and using Stripe.js, iOS, or Android client-side SDKs to handle the next actions. 213 | }; 214 | 215 | paymentIntent = await stripe.paymentIntents.create(paymentIntentParam); 216 | } catch (error) { 217 | logError("", "initStripe", JSON.stringify(error), orderId); // id for audit log check for payment data 218 | } 219 | 220 | // store in paymentIntent result in db 221 | await models.payment_PaymentService.update( 222 | { 223 | transactionId: paymentIntent.id, 224 | response_param: JSON.stringify({ 225 | paymentIntent: paymentIntent, 226 | }), 227 | }, 228 | { 229 | where: { 230 | orderId, 231 | }, 232 | } 233 | ); 234 | 235 | return { 236 | paymentConfig, 237 | paymentData: insertPaymentServiceRes, 238 | paymentIntent, 239 | }; 240 | }; 241 | 242 | const stripeCallback = async (params, body, query) => { 243 | // status = succeeded, processing, requires_payment_method 244 | const { id } = params; // for isa2u id is parent id, others id is sales order id 245 | const { payment_intent, payment_intent_client_secret, redirect_status } = query; 246 | 247 | // if other than succeeded, processing, requires_payment_method status 248 | if (!["succeeded", "processing", "requires_payment_method"].includes(redirect_status)) { 249 | logError("", "stripeCallback_update_error", JSON.stringify(query), id); 250 | } 251 | 252 | // success 253 | if (redirect_status === "succeeded") { 254 | // retrieve payment data 255 | const paymentData = await models.payment_PaymentService.findOne({ 256 | where: { 257 | orderId: +id, 258 | paymentType: 7, // stripe 259 | }, 260 | }); 261 | 262 | let insertResParam = JSON.parse(paymentData.response_param); 263 | insertResParam.response = query; 264 | 265 | // proceeed to update response and update status 266 | const sqlquery = ` 267 | UPDATE payment_PaymentService 268 | SET status = :status, 269 | transactionDate = :transactionDate, 270 | updatedDtm = :updatedDtm, 271 | response_param = :responseParam 272 | WHERE orderId = :orderId 273 | `; 274 | 275 | const replacements = { 276 | status: "paid", 277 | transactionDate: moment().unix(), 278 | // paymentMode: PymtMethod, // paymentMode = :paymentMode, 279 | updatedDtm: moment().format("YYYY-MM-DD HH:mm:ss"), 280 | responseParam: JSON.stringify(insertResParam), // Assuming responseParamValue is your parameter value 281 | orderId: id, 282 | }; // Bind values to the query 283 | 284 | try { 285 | const [updatedRowsCount, updatedRows] = await sequelize.query(sqlquery, { 286 | replacements, 287 | type: sequelize.QueryTypes.UPDATE, 288 | }); 289 | if (updatedRows > 0) { 290 | // pass data to sales api stripe process 291 | try { 292 | const updateSalesApi = await axios.post(`${modulePATH("SALES")}api/salesorder/stripe/process/${id}`); 293 | } catch (error) { 294 | logError("", "StripeCallback_stripeProcess", JSON.stringify(error?.response?.data), id); 295 | } 296 | } 297 | } catch (error) { 298 | logError("", "StripeCallback_update", JSON.stringify(error), id); 299 | } 300 | } 301 | 302 | return redirection(); 303 | }; 304 | 305 | // cronjob to query unpaid data and sync data with stripe 306 | const StripeStatusCheck = async () => { 307 | try { 308 | const paymentData = await models.payment_PaymentService.findAll({ 309 | where: { 310 | status: "init", 311 | paymentType: 7, // stripe 312 | expiredDtm: { 313 | [Op.gte]: moment(), // ec2 is utc 314 | // [Op.gte]: moment().add(-8, "hour"), // -8 is because in db is utc, local is +8 315 | }, 316 | // response_param: { 317 | // [Op.eq]: null, // Checks for non-null values 318 | // }, // no include this because init will have data 319 | }, 320 | }); 321 | 322 | for (let i = 0; i < paymentData.length; i++) { 323 | const pData = paymentData[i]; 324 | 325 | const paymentConfig = await models.payment_cust_paymentMethod_config.findOne({ 326 | where: { 327 | paymentType: 7, // stripe 328 | [Op.or]: [{ merchantBranchId: pData.merchantBranchId }, { merchantBranchId: 0 }], 329 | countryId: pData.countryId, 330 | }, 331 | order: [ 332 | ["merchantBranchId", "DESC"], // Prefer records with variableMerchantBranchId if both exist 333 | ], 334 | }); 335 | 336 | const stripe = new Stripe(paymentConfig.merchantSecret); 337 | const paymentIntent = await stripe.paymentIntents.retrieve(pData.transactionId); 338 | 339 | if (paymentIntent.status === "succeeded") { 340 | let insertResParam = JSON.parse(pData.response_param); 341 | insertResParam.cron_response = paymentIntent; 342 | 343 | // proceeed to update response and update status 344 | const sqlquery = ` 345 | UPDATE payment_PaymentService 346 | SET status = :status, 347 | transactionDate = :transactionDate, 348 | updatedDtm = :updatedDtm, 349 | response_param = :responseParam 350 | WHERE orderId = :orderId 351 | `; 352 | const replacements = { 353 | status: "paid", 354 | transactionDate: moment().unix(), 355 | // paymentMode: PymtMethod, // paymentMode = :paymentMode, 356 | updatedDtm: moment().format("YYYY-MM-DD HH:mm:ss"), 357 | responseParam: JSON.stringify(insertResParam), // Assuming responseParamValue is your parameter value 358 | orderId: pData.orderId, 359 | }; // Bind values to the query 360 | 361 | try { 362 | const [updatedRowsCount, updatedRows] = await sequelize.query(sqlquery, { 363 | replacements, 364 | type: sequelize.QueryTypes.UPDATE, 365 | }); 366 | if (updatedRows > 0) { 367 | // pass data to sales api stripe process 368 | try { 369 | const updateSalesApi = await axios.post(`${modulePATH("SALES")}api/salesorder/stripe/process/${pData.orderId}`); 370 | } catch (error) { 371 | logError("", "StripeCallback_stripeProcess", JSON.stringify(error?.response?.data), pData.orderId); 372 | } 373 | } 374 | } catch (error) { 375 | logError("", "StripeCallback_update", JSON.stringify(error), pData.orderId); 376 | } 377 | } 378 | } 379 | } catch (error) { 380 | console.error(error); 381 | logError("", "CrobJob:StripeStatusCheck", JSON.stringify(error), ""); 382 | } 383 | }; 384 | 385 | const queryStripeOrder = async (req, res, next) => { 386 | try { 387 | const tx_id = req.params.tx_id; 388 | const rows = await pool.execute("SELECT * FROM transactions WHERE transactionId = ?", [tx_id]); 389 | if (row.length === 0) { 390 | res.status(404).json({ error: "Transaction not found" }); 391 | } else { 392 | res.status(200).json(rows[0]); 393 | } 394 | } catch (error) { 395 | console.error(error); 396 | res.status(500).json({ error: "Error fetching transaction status" }); 397 | } 398 | }; 399 | 400 | module.exports = { initStripe, queryStripeOrder, stripeCallback, StripeStatusCheck }; 401 | --------------------------------------------------------------------------------