UNPKG

@lomray/microservice-payment-stripe

Version:

Stripe payment microservice based on NodeJS & inverted json.

986 lines (984 loc) 67.1 kB
'use strict'; var tslib = require('tslib'); var microserviceHelpers = require('@lomray/microservice-helpers'); var microserviceNodejsLib = require('@lomray/microservice-nodejs-lib'); var Event = require('@lomray/microservices-client-api/constants/events/payment-stripe'); var fromSmallestUnit = require('@lomray/microservices-client-api/helpers/parsers/from-smallest-unit'); var toSmallestUnit = require('@lomray/microservices-client-api/helpers/parsers/to-smallest-unit'); var classValidator = require('class-validator'); var typeorm = require('typeorm'); var remote = require('../../config/remote.js'); var couponDuration = require('../../constants/coupon-duration.js'); var payoutMethod = require('../../constants/payout-method.js'); var payoutMethodType = require('../../constants/payout-method-type.js'); var stripePaymentMethods = require('../../constants/stripe-payment-methods.js'); var transactionDefaultParams = require('../../constants/transaction-default-params.js'); var transactionRole = require('../../constants/transaction-role.js'); var transactionStatus = require('../../constants/transaction-status.js'); var transactionType = require('../../constants/transaction-type.js'); var composeBalance = require('../../helpers/compose-balance.js'); var extractIdFromStripeInstance = require('../../helpers/extract-id-from-stripe-instance.js'); var fromExpirationDate = require('../../helpers/formatters/from-expiration-date.js'); var toExpirationDate = require('../../helpers/formatters/to-expiration-date.js'); var messages = require('../../helpers/validators/messages.js'); var card = require('../../repositories/card.js'); var calculation = require('../common/calculation.js'); var parser = require('../parser.js'); var index = require('../webhook-handlers/index.js'); var abstract = require('./abstract.js'); /** * Stripe payment provider */ class Stripe extends abstract { /** * Init service */ static init(manager = typeorm.getManager()) { return tslib.__awaiter(this, void 0, void 0, function* () { const { config, paymentMethods, apiKey, fees, taxes } = yield remote(); // All environments are required const isFeesDefined = Boolean((fees === null || fees === void 0 ? void 0 : fees.stablePaymentUnit) && (fees === null || fees === void 0 ? void 0 : fees.stableDisputeFeeUnit) && (fees === null || fees === void 0 ? void 0 : fees.paymentPercent) && (fees === null || fees === void 0 ? void 0 : fees.instantPayoutPercent)); const isTaxesDefined = Boolean((taxes === null || taxes === void 0 ? void 0 : taxes.autoCalculateFeeUnit) && (taxes === null || taxes === void 0 ? void 0 : taxes.stableUnit) && (taxes === null || taxes === void 0 ? void 0 : taxes.defaultPercent)); if (!config || !apiKey || !paymentMethods || !isFeesDefined || !isTaxesDefined) { throw new Error('Payment options or api key or payment methods for stripe are not provided'); } // Refers to the constructor. Used for correct init override in child. return new this(manager, apiKey, config, paymentMethods); }); } /** * Add new card * @description Definitions: * 1. Usage example - only in integration tests * 2. Use setup intent for live-mode * 3. For creating card manually with the sensitive data such as digits, cvc. Platform * account must be eligible for PCI (Payment Card Industry Data Security Standards) */ addCard(params) { return tslib.__awaiter(this, void 0, void 0, function* () { const customer = yield this.customerRepository.findOne({ where: { userId: params.userId } }); if (!customer) { throw new microserviceNodejsLib.BaseException({ status: 500, message: messages.getNotFoundMessage('Customer') }); } const cardData = Stripe.buildCardData(params); if (!cardData) { throw new microserviceNodejsLib.BaseException({ status: 400, message: 'Provided card data is invalid.' }); } /** * Create card as the payment method */ const { id, card: stripeCard } = yield this.sdk.paymentMethods.create({ type: 'card', card: cardData, expand: ['card'], }); if (!(stripeCard === null || stripeCard === void 0 ? void 0 : stripeCard.exp_month) || !(stripeCard === null || stripeCard === void 0 ? void 0 : stripeCard.exp_year)) { throw new microserviceNodejsLib.BaseException({ status: 500, message: 'Failed to get card expiration date.' }); } /** * Attach card to customer */ yield this.sdk.paymentMethods.attach(id, { customer: customer.customerId, }); /** * Validate and save card */ const card = this.cardRepository.create({ expired: toExpirationDate(stripeCard.exp_month, stripeCard.exp_year), userId: params.userId, funding: stripeCard === null || stripeCard === void 0 ? void 0 : stripeCard.funding, brand: stripeCard === null || stripeCard === void 0 ? void 0 : stripeCard.brand, lastDigits: stripeCard === null || stripeCard === void 0 ? void 0 : stripeCard.last4, params: { paymentMethodId: id }, }); const errors = yield classValidator.validate(card, { whitelist: true, forbidNonWhitelisted: true, validationError: { target: false }, }); if (errors.length > 0) { throw new microserviceNodejsLib.BaseException({ status: 422, message: `Validation failed for card.`, payload: errors, }); } return this.cardRepository.save(card); }); } /** * Add bank account * @description Usage example - integration tests * @TODO: Integrate with stripe */ addBankAccount(_a) { var { bankAccountId } = _a, rest = tslib.__rest(_a, ["bankAccountId"]); return tslib.__awaiter(this, void 0, void 0, function* () { const bankAccount = this.bankAccountRepository.create(Object.assign(Object.assign({}, rest), { params: { bankAccountId } })); const errors = yield classValidator.validate(bankAccount, { whitelist: true, forbidNonWhitelisted: true, validationError: { target: false }, }); if (errors.length > 0) { throw new microserviceNodejsLib.BaseException({ status: 422, message: `Validation failed for bank account.`, payload: errors, }); } return this.bankAccountRepository.save(bankAccount); }); } /** * Create SetupIntent and return to client secret * @description Use on session usage for checkouts */ setupIntent(userId) { const _super = Object.create(null, { getCustomer: { get: () => super.getCustomer } }); return tslib.__awaiter(this, void 0, void 0, function* () { const { setupIntentUsage } = yield remote(); // Get related customer const { customerId } = yield _super.getCustomer.call(this, userId); const { client_secret: clientSecret } = yield this.sdk.setupIntents.create({ usage: setupIntentUsage, customer: customerId, // eslint-disable-next-line camelcase payment_method_types: this.methods, }); return clientSecret; }); } /** * Create Customer entity */ createCustomer(userId, email, name) { const _super = Object.create(null, { createCustomer: { get: () => super.createCustomer } }); return tslib.__awaiter(this, void 0, void 0, function* () { const { id } = yield this.sdk.customers.create({ name, email, }); return _super.createCustomer.call(this, userId, id); }); } /** * Remove Customer from db and stripe * @description Usage example - integration tests */ removeCustomer(userId) { return tslib.__awaiter(this, void 0, void 0, function* () { const customer = yield this.customerRepository.findOne({ userId }); if (!customer) { throw new microserviceNodejsLib.BaseException({ status: 400, message: messages.getNotFoundMessage('Customer') }); } const { deleted: isDeleted } = yield this.sdk.customers.del(customer.customerId); if (!isDeleted) { return false; } yield this.customerRepository.remove(customer); return true; }); } /** * Create Product entity */ createProduct(params) { const _super = Object.create(null, { createProduct: { get: () => super.createProduct } }); return tslib.__awaiter(this, void 0, void 0, function* () { const { entityId, name, description, images, userId } = params; const { id } = yield this.sdk.products.create({ name, description, images, }); return _super.createProduct.call(this, { entityId, userId, }, id); }); } /** * Create Price entity */ createPrice(params) { const _super = Object.create(null, { createPrice: { get: () => super.createPrice } }); return tslib.__awaiter(this, void 0, void 0, function* () { const { currency, unitAmount, productId, userId } = params; const { id } = yield this.sdk.prices.create({ currency, product: productId, // eslint-disable-next-line camelcase unit_amount: unitAmount, }); return _super.createPrice.call(this, { userId, productId, currency, unitAmount, }, id); }); } /** * Create checkout session and return url to redirect user for payment */ createCheckout(params) { const _super = Object.create(null, { getCustomer: { get: () => super.getCustomer } }); return tslib.__awaiter(this, void 0, void 0, function* () { const { priceId, userId, successUrl, cancelUrl, isAllowPromoCode } = params; const { customerId } = yield _super.getCustomer.call(this, userId); const price = yield this.priceRepository.findOne({ priceId }, { relations: ['product'] }); if (!price) { microserviceHelpers.Log.error(`There is no price related to this priceId: ${priceId}`); return null; } /* eslint-disable camelcase */ const { id, url } = yield this.sdk.checkout.sessions.create({ line_items: [ { price: priceId, quantity: 1, }, ], mode: 'payment', customer: customerId, success_url: successUrl, cancel_url: cancelUrl, allow_promotion_codes: isAllowPromoCode, }); /* eslint-enable camelcase */ yield this.createTransaction({ type: transactionType.CREDIT, amount: price.unitAmount, userId, productId: price.productId, entityId: price.product.entityId, status: transactionStatus.INITIAL, }, id); return url; }); } /** * Create checkout session for existing cart and return url to redirect user for payment * @TODO: get rid of the ts-ignores */ createCartCheckout(params) { const _super = Object.create(null, { getCustomer: { get: () => super.getCustomer } }); return tslib.__awaiter(this, void 0, void 0, function* () { const { cartId, userId, isEmbeddedMode, customerEmail } = params; const { customerId } = yield _super.getCustomer.call(this, userId); const cart = yield this.cartRepository.findOne({ id: cartId }, { relations: ['items', 'items.price'], }); if (!cart) { microserviceHelpers.Log.error(`There is no cart related to this cartId: ${cartId}`); return null; } const lineItems = cart.items.map(({ price, quantity }) => ({ price: price.priceId, quantity, })); /* eslint-disable camelcase */ let checkoutParams = { customer_email: customerEmail, line_items: lineItems, mode: 'payment', customer: customerId, locale: 'en', }; /** * Set redirect url for embedded mode or success/cancel urls for stripe hosted mode */ if (isEmbeddedMode) { const { returnUrl } = params; checkoutParams = Object.assign(Object.assign(Object.assign({}, checkoutParams), { // @ts-ignore ui_mode: 'embedded' }), (returnUrl ? { return_url: returnUrl } : { redirect_on_completion: 'never' })); // @ts-ignore } else { const { successUrl, cancelUrl } = params; checkoutParams = Object.assign(Object.assign({}, checkoutParams), { success_url: successUrl, cancel_url: cancelUrl }); } // @TODO: update version of the stripe SDK to get new types in sheckout sessions create const { id, url, // @ts-ignore client_secret: clientSecret, } = yield this.sdk.checkout.sessions.create(checkoutParams); /* eslint-enable camelcase */ yield this.createTransaction({ type: transactionType.CREDIT, amount: cart.items.reduce((acc, item) => acc + item.price.unitAmount * item.quantity, 0), userId, entityId: cart.id, status: transactionStatus.INITIAL, }, id); return { redirectUrl: url, clientSecret }; }); } /** * Connect account * @description Create ConnectAccount make redirect to account link and save stripeConnectAccount in customer */ connectAccount(userId, email, accountType, refreshUrl, returnUrl, businessType) { const _super = Object.create(null, { getCustomer: { get: () => super.getCustomer } }); return tslib.__awaiter(this, void 0, void 0, function* () { const customer = yield _super.getCustomer.call(this, userId); if (!customer.params.accountId) { const stripeConnectAccount = yield this.sdk.accounts.create(Object.assign(Object.assign({ type: accountType, country: 'US', email }, (businessType ? { business_type: businessType } : {})), { settings: { payouts: { // eslint-disable-next-line camelcase debit_negative_balances: true, // eslint-disable-next-line camelcase schedule: { interval: 'manual' }, }, } })); customer.params.accountId = stripeConnectAccount.id; customer.params.accountType = stripeConnectAccount.type; yield this.customerRepository.save(customer); } return (yield this.buildAccountLink(customer.params.accountId, refreshUrl, returnUrl)).url; }); } /** * Returns dashboard login link * @description Eligible only for the express connect accounts. * DO NOT email, text, or otherwise send login link URLs directly to user */ getDashboardLoginLink(userId) { return tslib.__awaiter(this, void 0, void 0, function* () { const customer = yield this.customerRepository.findOne({ userId }); if (!customer) { throw new microserviceNodejsLib.BaseException({ status: 400, message: messages.getNotFoundMessage('Customer'), }); } if (!customer.params.accountId) { throw new microserviceNodejsLib.BaseException({ status: 400, message: "Customer don't have setup connect account.", }); } if (customer.params.accountType !== 'express') { throw new microserviceNodejsLib.BaseException({ status: 500, message: 'Dashboard login allowed only for express accounts.', }); } return (yield this.sdk.accounts.createLoginLink(customer.params.accountId)).url; }); } /** * Returns account link * @description Use when user needs to update connect account data */ getConnectAccountLink(userId, refreshUrl, returnUrl) { return tslib.__awaiter(this, void 0, void 0, function* () { const customer = yield this.customerRepository.findOne({ userId }); if (!customer) { throw new microserviceNodejsLib.BaseException({ status: 400, message: messages.getNotFoundMessage('Customer'), }); } if (!customer.params.accountId) { throw new microserviceNodejsLib.BaseException({ status: 400, message: "Customer don't have setup connect account.", }); } return (yield this.buildAccountLink(customer.params.accountId, refreshUrl, returnUrl)).url; }); } /** * Get the webhook from stripe and handle deciding on type of event * @description If handlers can be used for connect and master account - wrap it in handlers callbacks */ handleWebhookEvent(payload, signature, webhookKey, webhookType) { return tslib.__awaiter(this, void 0, void 0, function* () { const event = this.sdk.webhooks.constructEvent(payload, signature, webhookKey); try { yield this.processWebhookEvent(event, webhookType); } catch (error) { const errorMessage = `Failed to process webhook. Event type: "${event.type}", webhook type: "${webhookType}". ${error.message} `; microserviceHelpers.Log.error(errorMessage); // Throw error for Stripe webhook retry throw new Error(errorMessage); } }); } /** * Create instant payout * @description Should be called from the API */ instantPayout({ userId, amount, entityId, payoutMethod: payoutMethod$1, currency = 'usd', }) { return tslib.__awaiter(this, void 0, void 0, function* () { const { payout } = yield remote(); const { instantMaxAmountPerTransactionUnit, instantMinAmountPerTransactionUnit } = payout; const payoutMethodAllowances = yield this.getPayoutMethodAllowances(userId, payoutMethod$1); if (!(payoutMethodAllowances === null || payoutMethodAllowances === void 0 ? void 0 : payoutMethodAllowances.isInstantPayoutAllowed)) { throw new microserviceNodejsLib.BaseException({ status: 400, message: "Provided payout method isn't support instant payout.", }); } const amountUnit = toSmallestUnit(amount); if (!amountUnit) { throw new microserviceNodejsLib.BaseException({ status: 500, message: 'Failed to validate requested instant payout amount.', }); } if (amountUnit > instantMaxAmountPerTransactionUnit) { throw new microserviceNodejsLib.BaseException({ status: 500, message: 'Requested amount is more than payout transaction limit.', }); } if (amountUnit < instantMinAmountPerTransactionUnit) { throw new microserviceNodejsLib.BaseException({ status: 500, message: 'Requested amount is less than payout transaction limit.', }); } // Get related customer const customer = yield this.customerRepository.findOne({ userId, }); if (!customer) { throw new microserviceNodejsLib.BaseException({ status: 400, message: messages.getNotFoundMessage('Customer'), }); } if (!customer.params.accountId) { throw new microserviceNodejsLib.BaseException({ status: 400, message: "Customer don't have related connect account.", }); } if (!customer.params.isPayoutEnabled) { throw new microserviceNodejsLib.BaseException({ status: 400, message: "Payout isn't available.", }); } const { instant_available: instantBalance } = yield this.sdk.balance.retrieve({ stripeAccount: customer.params.accountId, }); if (!instantBalance) { throw new microserviceNodejsLib.BaseException({ status: 500, message: "Instant balance isn't available", }); } const balance = composeBalance(instantBalance); if (!(balance === null || balance === void 0 ? void 0 : balance[currency])) { throw new microserviceNodejsLib.BaseException({ status: 400, message: `Balance with the ${currency} isn't available.`, }); } if ((balance === null || balance === void 0 ? void 0 : balance[currency]) < amountUnit) { throw new microserviceNodejsLib.BaseException({ status: 400, message: `Insufficient funds. Instant balance is ${balance === null || balance === void 0 ? void 0 : balance[currency]} in ${currency}.`, }); } let stripePayout; try { stripePayout = yield this.sdk.payouts.create({ currency, amount: amountUnit, method: payoutMethod.INSTANT, destination: payoutMethodAllowances.externalAccountId, }, // Payout user connected account funds { stripeAccount: customer.params.accountId }); } catch (error) { microserviceHelpers.Log.error(error.message); throw new microserviceNodejsLib.BaseException({ status: 500, message: 'Stripe instant payout was failed.', payload: { message: error.message, }, }); } const { id: payoutId, method, arrival_date: arrivalDate, description, destination, created, status, type, failure_code: failureCode, failure_message: failureMessage, } = stripePayout; const payoutEntity = this.payoutRepository.create(Object.assign(Object.assign({ amount: amountUnit, arrivalDate: new Date(Number(arrivalDate) * 1000), method: method, payoutId, description, failureCode, failureMessage, currency, type: parser.parseStripePayoutType(type), status: parser.parseStripePayoutStatus(status), registeredAt: new Date(Number(created) * 1000) }, (entityId ? { entityId } : {})), (destination ? { destination: extractIdFromStripeInstance(destination) } : {}))); yield this.payoutRepository.save(payoutEntity); return true; }); } /** * Returns user related connect account balance */ getBalance(userId) { return tslib.__awaiter(this, void 0, void 0, function* () { const customer = yield this.customerRepository.findOne({ userId, }); if (!customer) { throw new microserviceNodejsLib.BaseException({ status: 400, message: messages.getNotFoundMessage('Customer'), }); } if (!customer.params.accountId) { throw new microserviceNodejsLib.BaseException({ status: 400, message: "Customer don't have related connect account", }); } const { available, pending, instant_available: instant = [], } = yield this.sdk.balance.retrieve({ stripeAccount: customer.params.accountId, }); return { available: composeBalance(available), instant: composeBalance(instant), pending: composeBalance(pending), }; }); } /** * Handles completing of transaction inside stripe payment process */ handleTransactionCompleted(event) { return tslib.__awaiter(this, void 0, void 0, function* () { const { id, payment_status: paymentStatus, status, amount_total: amountTotal, } = event.data.object; const transaction = yield this.transactionRepository.findOne({ transactionId: id }); if (!transaction) { microserviceHelpers.Log.error(`There is no actual transfer for entity with following transaction id: ${id}`); } yield this.transactionRepository.update({ transactionId: id }, { status: parser.parseStripeTransactionStatus(paymentStatus), amount: amountTotal, params: { checkoutStatus: status, paymentStatus: paymentStatus, }, }); void microserviceNodejsLib.Microservice.eventPublish(Event.EntityPaid, { entityId: transaction === null || transaction === void 0 ? void 0 : transaction.entityId, userId: transaction === null || transaction === void 0 ? void 0 : transaction.userId, }); }); } /** * Create transfer for connected account */ createTransfer(entityId, userId, payoutCoeff) { return tslib.__awaiter(this, void 0, void 0, function* () { const transfer = yield this.getTransferInfo(entityId, userId); const product = yield this.productRepository.findOne({ entityId }); if (!transfer || !product) { microserviceHelpers.Log.error(`There is no actual transfers or product for entity with following id: ${entityId}`); return; } const { id } = yield this.sdk.transfers.create({ amount: Math.ceil(transfer.amount * payoutCoeff), currency: 'usd', destination: transfer.destinationUser, }); const transaction = this.transactionRepository.create({ transactionId: id, userId: transfer.userId, entityId, amount: Math.ceil(transfer.amount * payoutCoeff), type: transactionType.DEBIT, status: transactionStatus.INITIAL, product: { productId: product.productId, }, }); yield this.transactionRepository.save(transaction); }); } /** * Attach to transactions charge refs (transfer, destination payment and related amounts) */ attachToTransactionsChargeRefs(chargeId) { return tslib.__awaiter(this, void 0, void 0, function* () { const transactions = yield this.transactionRepository .createQueryBuilder('t') .where('t.chargeId = :chargeId', { chargeId }) .getMany(); if (!transactions.length) { const errorMessage = messages.getNotFoundMessage('Failed to get charge regs. Debit or credit transaction'); microserviceHelpers.Log.error(errorMessage); throw new microserviceNodejsLib.BaseException({ status: 500, message: errorMessage, payload: { transactions: transactions.map(({ transactionId, type, id }) => ({ transactionId, type, id, })), }, }); } /** * Get transfer and application fees * @description Transfer only for destination charges. * The reason for managing application fees here is that when an application fee is created, * Stripe sends an event in parallel with the creation of the payment intent. Currently, * the database does not support transactions at this moment. */ const { transfer, application_fee: applicationFee } = yield this.sdk.charges.retrieve(chargeId, { expand: ['application_fee'], }); const isApplicationFeeExpanded = Stripe.checkIfApplicationFeeIsObject(applicationFee); if (!isApplicationFeeExpanded) { const errorMessage = 'Failed to expand charge application fee'; microserviceHelpers.Log.error(errorMessage); throw new microserviceNodejsLib.BaseException({ status: 500, message: errorMessage, payload: { applicationFee }, }); } const { id: applicationFeeId, amount: applicationFeeAmount, amount_refunded: applicationFeeRefundedAmount, } = applicationFee; if (transactions.some(({ fee }) => fee !== applicationFeeAmount)) { const errorMessage = 'Failed to update transaction application fee. Application fee do not equal to transaction fee'; microserviceHelpers.Log.error(errorMessage); throw new microserviceNodejsLib.BaseException({ status: 500, message: errorMessage, payload: { applicationFeeAmount, transactionsFee: JSON.stringify(transactions.map(({ fee }) => ({ fee }))), }, }); } const transferId = transfer ? extractIdFromStripeInstance(transfer) : null; const isDestinationTransaction = transactions.some(({ params }) => params.transferDestinationConnectAccountId); const destinationTransactionErrorMessage = messages.getNotFoundMessage('Failed to retrieve charge transfer destination transaction'); let transferExpanded = null; /** * If transaction is destination and transfer was not retrieved */ if (isDestinationTransaction && !transferId) { microserviceHelpers.Log.error(destinationTransactionErrorMessage); throw new microserviceNodejsLib.BaseException({ status: 500, message: destinationTransactionErrorMessage, }); } /** * Get destination transaction * @description Only for destination charges (e.g. destination payment intent) */ if (transferId) { transferExpanded = yield this.sdk.transfers.retrieve(transferId); if (!(transferExpanded === null || transferExpanded === void 0 ? void 0 : transferExpanded.destination_payment)) { microserviceHelpers.Log.error(destinationTransactionErrorMessage); throw new microserviceNodejsLib.BaseException({ status: 500, message: destinationTransactionErrorMessage, }); } } const destinationTransaction = transferId && transferExpanded ? extractIdFromStripeInstance(transferExpanded) : null; transactions.forEach((transaction) => { var _a, _b; transaction.applicationFeeId = applicationFeeId; transaction.params.transferId = transferId; transaction.params.refundedApplicationFeeAmount = applicationFeeRefundedAmount; if (destinationTransaction) { transaction.params.destinationTransactionId = extractIdFromStripeInstance(destinationTransaction); transaction.params.transferAmount = (_a = transferExpanded === null || transferExpanded === void 0 ? void 0 : transferExpanded.amount) !== null && _a !== void 0 ? _a : 0; transaction.params.transferReversedAmount = (_b = transferExpanded === null || transferExpanded === void 0 ? void 0 : transferExpanded.amount_reversed) !== null && _b !== void 0 ? _b : 0; } }); yield this.transactionRepository.save(transactions); }); } /** * Create PaymentIntent */ createPaymentIntent({ userId, entityCost, receiverId, cardId, title, applicationPaymentPercent, entityId, additionalFeesPercent, extraReceiverRevenuePercent, withTax, feesPayer = transactionRole.SENDER, }) { var _a, _b, _c, _d, _e; return tslib.__awaiter(this, void 0, void 0, function* () { const { fees } = yield remote(); const { instantPayoutPercent = 1 } = fees; const { sender: senderCustomer, receiver: receiverCustomer } = yield this.getAndValidateTransactionContributors(userId, receiverId); // Verify if customer is verified const { userId: receiverUserId, params: { accountId: receiverAccountId }, } = receiverCustomer; const chargeCard = yield this.getChargingCard(senderCustomer.userId, cardId); const paymentMethodId = card.extractPaymentMethodId(chargeCard); if (!paymentMethodId) { throw new microserviceNodejsLib.BaseException({ status: 500, message: messages.getNotFoundMessage('Payment intent creation is failed. Payment method'), }); } const { id: paymentMethodCardId } = chargeCard; // Get parsed entity cost const entityUnitCost = toSmallestUnit(entityCost); if (!entityUnitCost) { throw new microserviceNodejsLib.BaseException({ status: 500, message: 'Failed to calculate entity cost unit amount.', }); } // Calculate not-tax transaction fees const { userUnitAmount, receiverUnitRevenue, platformUnitFee, stripeUnitFee: paymentIntentStripeFeeUnit, receiverAdditionalFee, extraReceiverUnitRevenue, senderAdditionalFee, } = yield calculation.getPaymentIntentFees({ entityUnitCost, applicationPaymentPercent, feesPayer, additionalFeesPercent, extraReceiverRevenuePercent, // If with tax - do not include Stripe transaction fee withStripeFee: !withTax, }); // Group up payment intent data let taxFeeUnit = 0; let tax = null; let stripeFeeUnit = null; let paymentIntentAmountUnit = null; let taxAutoCalculateFeeUnit = null; if (withTax) { if (!entityId) { throw new microserviceNodejsLib.BaseException({ status: 400, message: 'Entity reference is required for tax calculation.', }); } const { tax: taxData, createTaxTransactionFeeUnit: taxFeeData, autoCalculateFeeUnit, } = yield calculation.getPaymentIntentTax(this.sdk, { entityId, processingTransactionAmountUnit: userUnitAmount, paymentMethodId, feesPayer, }); taxAutoCalculateFeeUnit = autoCalculateFeeUnit; tax = taxData; // Included in transaction tax fee unit that will be covered by fees payer taxFeeUnit = taxFeeData; const { stripeFeeUnit: transactionFeeUnit, processingAmountUnit } = yield calculation.getStripeFeeAndProcessingAmount({ amountUnit: taxData === null || taxData === void 0 ? void 0 : taxData.transactionAmountWithTaxUnit, feesPayer, }); stripeFeeUnit = transactionFeeUnit; paymentIntentAmountUnit = processingAmountUnit; } else { stripeFeeUnit = paymentIntentStripeFeeUnit; paymentIntentAmountUnit = userUnitAmount; } // Prevent type error cause on payment intent metadata and transaction params const sharedTaxData = { taxCreatedAt: (_a = tax === null || tax === void 0 ? void 0 : tax.createdAt) === null || _a === void 0 ? void 0 : _a.toISOString(), taxExpiresAt: (_b = tax === null || tax === void 0 ? void 0 : tax.expiresAt) === null || _b === void 0 ? void 0 : _b.toISOString(), taxBehaviour: tax === null || tax === void 0 ? void 0 : tax.behaviour, totalTaxPercent: tax === null || tax === void 0 ? void 0 : tax.totalTaxPercent, }; const baseFeeUnit = platformUnitFee + stripeFeeUnit + taxFeeUnit; const senderPersonalFeeUnit = baseFeeUnit + senderAdditionalFee; const receiverPersonalFeeUnit = baseFeeUnit + receiverAdditionalFee; const collectedFeeUnit = baseFeeUnit + senderAdditionalFee + receiverAdditionalFee + ((_c = tax === null || tax === void 0 ? void 0 : tax.totalAmountUnit) !== null && _c !== void 0 ? _c : 0); /* eslint-disable camelcase */ const stripePaymentIntent = yield this.sdk.paymentIntents.create(Object.assign(Object.assign({}, (title ? { description: title } : {})), { metadata: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ feesPayer, senderId: senderCustomer.userId, cardId: paymentMethodCardId, receiverId: receiverUserId, // Original float entity cost entityCost, stripeFee: fromSmallestUnit(stripeFeeUnit), platformFee: fromSmallestUnit(platformUnitFee), receiverExtraFee: fromSmallestUnit(receiverAdditionalFee), senderExtraFee: fromSmallestUnit(senderAdditionalFee), receiverExtraRevenue: fromSmallestUnit(extraReceiverUnitRevenue), receiverRevenue: fromSmallestUnit(receiverUnitRevenue), baseFee: fromSmallestUnit(baseFeeUnit), senderPersonalFee: fromSmallestUnit(senderPersonalFeeUnit), receiverPersonalFee: fromSmallestUnit(receiverPersonalFeeUnit), fee: fromSmallestUnit(collectedFeeUnit) }, (entityId ? { entityId } : {})), (title ? { description: title } : {})), ((tax === null || tax === void 0 ? void 0 : tax.id) ? { taxCalculationId: tax === null || tax === void 0 ? void 0 : tax.id } : {})), (Object.keys(sharedTaxData).length !== 0 ? Object.assign({}, sharedTaxData) : {})), (taxAutoCalculateFeeUnit ? { taxAutoCalculateFee: fromSmallestUnit(taxAutoCalculateFeeUnit) } : {})), (taxFeeUnit ? { taxFee: fromSmallestUnit(taxFeeUnit) } : {})), ((tax === null || tax === void 0 ? void 0 : tax.transactionAmountWithTaxUnit) ? { taxTransactionAmountWithTax: fromSmallestUnit(tax === null || tax === void 0 ? void 0 : tax.transactionAmountWithTaxUnit), } : {})), ((tax === null || tax === void 0 ? void 0 : tax.totalAmountUnit) ? { taxTotalAmount: fromSmallestUnit(tax === null || tax === void 0 ? void 0 : tax.totalAmountUnit) } : {})), payment_method_types: [stripePaymentMethods.CARD], confirm: true, currency: 'usd', capture_method: 'automatic', payment_method: paymentMethodId, customer: senderCustomer.customerId, // How much must sender must pay amount: paymentIntentAmountUnit, // How much application will collect fee application_fee_amount: paymentIntentAmountUnit - receiverUnitRevenue, transfer_data: { destination: receiverAccountId, } })); const transactionData = Object.assign(Object.assign({ entityId, title, paymentMethodId, cardId: paymentMethodCardId, transactionId: stripePaymentIntent.id, fee: collectedFeeUnit }, (tax ? { tax: tax.totalAmountUnit, taxCalculationId: tax.id } : {})), { params: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, transactionDefaultParams), { feesPayer, platformFee: platformUnitFee, stripeFee: stripeFeeUnit, entityCost: entityUnitCost, baseFee: baseFeeUnit }), (Object.keys(sharedTaxData).length !== 0 ? Object.assign({}, sharedTaxData) : {})), (taxAutoCalculateFeeUnit ? { taxAutoCalculateFee: taxAutoCalculateFeeUnit } : {})), (taxFeeUnit ? { taxFee: taxFeeUnit } : {})), { taxTransactionAmountWithTaxUnit: tax === null || tax === void 0 ? void 0 : tax.transactionAmountWithTaxUnit, taxTotalAmountUnit: tax === null || tax === void 0 ? void 0 : tax.totalAmountUnit }) }); const transactions = yield Promise.all([ this.transactionRepository.save(this.transactionRepository.create(Object.assign(Object.assign({}, transactionData), { userId: senderCustomer.userId, type: transactionType.CREDIT, amount: paymentIntentAmountUnit, params: Object.assign(Object.assign({}, transactionData.params), { extraFee: senderAdditionalFee, personalFee: senderPersonalFeeUnit }) }))), this.transactionRepository.save(this.transactionRepository.create(Object.assign(Object.assign({}, transactionData), { userId: receiverUserId, type: transactionType.DEBIT, amount: receiverUnitRevenue, // Amount that will be charge for instant payout params: Object.assign(Object.assign({}, transactionData.params), { extraFee: receiverAdditionalFee, personalFee: receiverPersonalFeeUnit, extraRevenue: extraReceiverUnitRevenue, estimatedInstantPayoutFee: Math.round(receiverUnitRevenue * (instantPayoutPercent / 100)) }) }))), ]); // Sync payment intent with the microservice transactions void this.sdk.paymentIntents.update(stripePaymentIntent.id, { metadata: { creditTransactionId: (_d = transactions === null || transactions === void 0 ? void 0 : transactions[0]) === null || _d === void 0 ? void 0 : _d.id, debitTransactionId: (_e = transactions === null || transactions === void 0 ? void 0 : transactions[1]) === null || _e === void 0 ? void 0 : _e.id, }, }); return transactions; }); } /** * Creates payout transfers for given entities */ payout(entitiesIds) { return tslib.__awaiter(this, void 0, void 0, function* () { const { payoutCoeff } = yield remote(); // TODO: create mechanism to rearrange/mark payouts with errors to deal with them later if (!payoutCoeff) { microserviceHelpers.Log.error('Payout coefficient is not provided'); return false; } yield Promise.allSettled(entitiesIds.map(({ id, userId }) => this.createTransfer(id, userId, payoutCoeff))); return true; }); } /** * Set default customer payment method */ setDefaultCustomerPaymentMethod(customerId, paymentMethodId) { return tslib.__awaiter(this, void 0, void 0, function* () { const customer = yield this.sdk.customers.update(customerId, { // eslint-disable-next-line camelcase invoice_settings: { // eslint-disable-next-line camelcase default_payment_method: paymentMethodId, }, }); return customer.invoice_settings.default_payment_method === paymentMethodId; }); } /** * Set default customer payment method */ removeCustomerPaymentMethod(paymentMethodId) { return tslib.__awaiter(this, void 0, void 0, function* () { yield this.sdk.paymentMethods.detach(paymentMethodId); return true; }); } /** * Create stripe promo code */ createPromoCode({ couponId, code: userCode, maxRedemptions, }) { return tslib.__awaiter(this, void 0, void 0, function* () { const { id, code } = yield this.sdk.promotionCodes.create({ coupon: couponId, code: userCode, // eslint-disable-next-line camelcase max_redemptions: maxRedemptions, }); return { id, code, }; }); } /** * Remove stripe coupon */ removeCoupon(couponId) { return tslib.__awaiter(this, void 0, void 0, function* () { const { deleted: isDeleted } = yield this.sdk.coupons.del(couponId); return isDeleted; }); } /** * Create stripe coupon */ createCoupon({ userId, name, currency, products, percentOff, amountOff, maxRedemptions, duration, durationInMonths, }) { const _super = Object.create(null, { createCoupon: { get: () => super.createCoupon } }); return tslib.__awaiter(this, void 0, void 0, function* () { const couponDiscount = Stripe.validateAndTransformCouponDiscountInput({ percentOff, amountOff, }); const couponDuration = Stripe.validateAndTransformCouponDurationInput({ duration, durationInMonths, }); const { id } = yield this.sdk.coupons.create(Object.assign(Object.assign(Object.assign({ name, currency: currency || 'usd' }, couponDiscount), couponDuration), { // eslint-disable-next-line camelcase applies_to: { products, } })); return _super.createCoupon.call(this, { userId, name, products, percentOff, amountOff, maxRedemptions, duration, durationInMonths, }, id); }); } /** * Get and validate receiver and sender */ getAndValidateTransactionContributors(senderId, receiverId) { return tslib.__awaiter(this, void 0, void 0, function* () { const sender = yield this.customerRepository.findOne({ userId: senderId }); const receiver = yield this.customerRepository.findOne({ userId: receiverId }); if (!sender) { throw new microserviceNodejsLib.BaseException({ status: 400, message: messages.getNotFoundMessage('Sender customer account'), }); } if (!receiver) { throw new microserviceNodejsLib.BaseException({ status: 400, message: messages.getNotFoundMessage('Receiver customer account'), }); } const { params: { accountId: receiverAccountId, isVerified: isReceiverVerified }, } = receiver; if (!receiverAccountId || !isReceiverVerified) { throw new microserviceNodejsLib.BaseException({ status: 400, message: "Receiver don't have setup or verified connected account.", }); } return { sender, receiver, }; }); } /** * Returns payout method data */ getPayoutMethodAllowances(userId, payoutMethod) { var _a, _b, _c, _d, _e; return tslib.__awaiter(this, void 0, void 0, function* () { const externalAccountNotFoundError = messages.getNotFoundMessage('External account for instant payout'); if (!payoutMethod) { const queries = [ this.bankAccountRepository.createQueryBuilder('pm'), this.cardRepository.createQueryBuilder('pm'), ]; const selectQueries = queries.map((query) => query .where('pm.userId =