@lomray/microservice-payment-stripe
Version:
Stripe payment microservice based on NodeJS & inverted json.
986 lines (984 loc) • 67.1 kB
JavaScript
'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 =