UNPKG

@wepublish/api

Version:
336 lines 16.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PayrexxSubscriptionPaymentProvider = void 0; const tslib_1 = require("tslib"); const client_1 = require("@prisma/client"); const api_1 = require("../../../../utils-api/src"); const crypto = tslib_1.__importStar(require("crypto")); const crypto_1 = require("crypto"); const add_1 = tslib_1.__importDefault(require("date-fns/add")); const parseISO_1 = tslib_1.__importDefault(require("date-fns/parseISO")); const startOfDay_1 = tslib_1.__importDefault(require("date-fns/startOfDay")); const sub_1 = tslib_1.__importDefault(require("date-fns/sub")); const node_fetch_1 = tslib_1.__importDefault(require("node-fetch")); const qs_1 = tslib_1.__importDefault(require("qs")); const payment_provider_1 = require("./payment-provider"); function mapPayrexxEventToPaymentStatus(event) { switch (event) { case 'waiting': return client_1.PaymentState.processing; case 'confirmed': return client_1.PaymentState.paid; case 'cancelled': return client_1.PaymentState.canceled; case 'declined': return client_1.PaymentState.declined; default: return null; } } function timeConstantCompare(a, b) { try { return (0, crypto_1.timingSafeEqual)(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8')); } catch (_a) { return false; } } function findSubscriptionByExternalId(subscriptionClient, externalId) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return subscriptionClient.findFirst({ where: { properties: { some: { key: 'payrexx_external_id', value: `${externalId}` } } }, include: { properties: true, deactivation: true, memberPlan: true, periods: { include: { invoice: true } } } }); }); } function deletePeriodOfUnpaidInvoice(subscriptionPeriodClient, subscription, invoice) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return subscriptionPeriodClient.deleteMany({ where: { invoiceID: invoice.id } }); }); } function deleteUnpaidInvoices(invoiceClient, subscriptionPeriodClient, subscription) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const unpaidInvoices = yield invoiceClient.findMany({ where: { subscriptionID: subscription.id, paidAt: null, canceledAt: null } }); for (const unpaidInvoice of unpaidInvoices) { yield deletePeriodOfUnpaidInvoice(subscriptionPeriodClient, subscription, unpaidInvoice); yield invoiceClient.delete({ where: { id: unpaidInvoice.id } }); } }); } class PayrexxSubscriptionPaymentProvider extends payment_provider_1.BasePaymentProvider { constructor(props) { super(props); this.instanceName = props.instanceName; this.instanceAPISecret = props.instanceAPISecret; this.webhookSecret = props.webhookSecret; this.remoteManagedSubscription = true; this.prisma = props.prisma; } updateRemoteSubscriptionAmount(props) { return tslib_1.__awaiter(this, void 0, void 0, function* () { // Find external id property and fail if subscription has been deactivated const properties = props.subscription.properties; const isPayrexxExt = properties.find(sub => sub.key === 'payrexx_external_id'); if (!isPayrexxExt) { throw new Error(`Payrexx Subscription Id not found on subscription ${props.subscription.id}`); } const amount = props.newAmount * (0, api_1.mapPaymentPeriodToMonths)(props.subscription.paymentPeriodicity); yield this.updateAmountUpstream(parseInt(isPayrexxExt.value, 10), amount.toString()); }); } cancelRemoteSubscription(props) { return tslib_1.__awaiter(this, void 0, void 0, function* () { // Find external id property and fail if subscription has been deactivated const properties = props.subscription.properties; const isPayrexxExt = properties.find(sub => sub.key === 'payrexx_external_id'); if (!isPayrexxExt) { throw new Error(`Payrexx Subscription Id not found on subscription ${props.subscription.id}`); } // Doing actual upstream cancellation yield this.cancelSubscriptionUpstream(parseInt(isPayrexxExt.value, 10)); }); } updatePaymentWithIntentState({ intentState, paymentClient, subscriptionClient, userClient, invoiceClient, subscriptionPeriodClient, invoiceItemClient }) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const apiData = JSON.parse(intentState.paymentData ? intentState.paymentData : '{}'); const rawSubscription = apiData.subscription; const subscriptionId = rawSubscription.id; if (intentState.state === client_1.PaymentState.paid) { const subscriptionValidUntil = (0, startOfDay_1.default)((0, parseISO_1.default)(rawSubscription.valid_until)); // Get subscription const subscription = yield findSubscriptionByExternalId(subscriptionClient, subscriptionId); if (!subscription) { (0, api_1.logger)('payrexxSubscriptionPaymentProvider').warn(`Subscription ${subscriptionId} received from payrexx webhook not found!`); return; } // Calculate max possible Extension length for subscription security margin of 7 days const maxSubscriptionExtensionLength = (0, sub_1.default)(subscriptionValidUntil, { days: 7 }); // Find last paid period in array let longestPeriod; for (const period of subscription.periods) { if (period.invoice.paidAt && (!longestPeriod || period.endsAt > longestPeriod.endsAt)) { longestPeriod = period; } } // If no period is found throw error if (!longestPeriod) throw new Error(`No period found in subscription ${subscriptionId}`); // Skip if subscription is already renewed if (maxSubscriptionExtensionLength <= (0, startOfDay_1.default)(longestPeriod.endsAt)) { (0, api_1.logger)('payrexxSubscriptionPaymentProvider').warn(`Received webhook for subscription ${subscriptionId} which is already renewed: ${maxSubscriptionExtensionLength.toISOString()} <= ${(0, startOfDay_1.default)(longestPeriod.endsAt).toISOString()}`); return; } // Calculate new subscription valid until const newSubscriptionValidUntil = (0, add_1.default)(longestPeriod.endsAt, { months: (0, api_1.mapPaymentPeriodToMonths)(subscription.paymentPeriodicity) }).toISOString(); const newSubscriptionValidFrom = (0, add_1.default)(longestPeriod.endsAt, { days: 1 }).toISOString(); // Get User const user = yield userClient.findUnique({ where: { id: subscription.userID } }); if (!user) throw new Error('User in subscription not found!'); // Get member plan const memberPlan = subscription.memberPlan; if (!memberPlan) throw new Error('Member Plan in subscription not found!'); const payedAmount = rawSubscription.invoice.amount; const minPayment = subscription.monthlyAmount * (0, api_1.mapPaymentPeriodToMonths)(subscription.paymentPeriodicity) - 100; // -1CHF to ensure that imported rounding differences are no issue if (payedAmount < minPayment) { (0, api_1.logger)('payrexxSubscriptionPaymentProvider').warn(`Payrexx Subscription ${subscription.id} payment ${payedAmount} lower than min payment ${minPayment}`); return; } // Delete unpaid yield deleteUnpaidInvoices(invoiceClient, subscriptionPeriodClient, subscription); // Create invoice const invoice = yield invoiceClient.create({ data: { mail: user.email, dueAt: new Date(), subscriptionID: subscription.id, description: `Abo ${memberPlan.name}`, paidAt: new Date(), canceledAt: null, scheduledDeactivationAt: (0, add_1.default)(new Date(), { days: 10 }) } }); yield invoiceItemClient.create({ data: { invoiceId: invoice.id, createdAt: new Date(), modifiedAt: new Date(), name: `Abo ${memberPlan.name}`, quantity: 1, amount: payedAmount } }); if (!invoice) throw new Error("Can't create Invoice"); // Add subscription Period const subscriptionPeriod = yield subscriptionPeriodClient.create({ data: { subscriptionId: subscription.id, startsAt: newSubscriptionValidFrom, endsAt: newSubscriptionValidUntil, paymentPeriodicity: subscription.paymentPeriodicity, amount: payedAmount, invoiceID: invoice.id } }); if (!subscriptionPeriod) throw new Error("Can't create subscription period"); // Create Payment const payment = yield paymentClient.create({ data: { paymentMethodID: subscription.paymentMethodID, state: client_1.PaymentState.paid, invoiceID: invoice.id } }); if (!payment) throw new Error("Can't create Payment"); // Update subscription yield subscriptionClient.update({ where: { id: subscription.id }, data: { paidUntil: newSubscriptionValidUntil } }); (0, api_1.logger)('payrexxSubscriptionPaymentProvider').info(`Subscription ${subscription.id} for user ${user.email} successfully renewed.`); } else { (0, api_1.logger)('payrexxSubscriptionPaymentProvider').info('External Auto renewal failed!'); } }); } updateAmountUpstream(subscriptionId, amount) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const data = { amount, currency: 'CHF' }; const signature = crypto .createHmac('sha256', this.instanceAPISecret) .update(qs_1.default.stringify(data)) .digest('base64'); const res = yield (0, node_fetch_1.default)(`https://api.payrexx.com/v1.0/Subscription/${subscriptionId}/?instance=${encodeURIComponent(this.instanceName)}`, { method: 'PUT', body: qs_1.default.stringify(Object.assign(Object.assign({}, data), { ApiSignature: signature })) }); const resJSON = yield res.json(); if (res.status === 200 && resJSON.status === 'success') { (0, api_1.logger)('payrexxSubscriptionPaymentProvider').info('Payrexx response for subscription %s updated', subscriptionId); } else { (0, api_1.logger)('payrexxSubscriptionPaymentProvider').error('Payrexx subscription update response for subscription %s is NOK with status %s and message %s', subscriptionId, res.status, resJSON.message); throw new Error(`Payrexx response is NOK with status ${res.status} and message: ${resJSON.message}`); } }); } cancelSubscriptionUpstream(subscriptionId) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const signature = crypto.createHmac('sha256', this.instanceAPISecret).digest('base64'); const res = yield (0, node_fetch_1.default)(`https://api.payrexx.com/v1.0/Subscription/${subscriptionId}/?instance=${this.instanceName}`, { method: 'DELETE', body: qs_1.default.stringify({ ApiSignature: signature }) }); const resJSON = yield res.json(); if (res.status === 200 && resJSON.status === 'success') { (0, api_1.logger)('payrexxSubscriptionPaymentProvider').info('Payrexx response for subscription %s canceled', subscriptionId); } else { (0, api_1.logger)('payrexxSubscriptionPaymentProvider').error('Payrexx subscription cancel response for subscription %s is NOK with status %s and message %s ', subscriptionId, res.status, resJSON.message); throw new Error(`Payrexx response is NOK with status ${res.status} and message: ${resJSON.message}`); } }); } webhookForPaymentIntent(props) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const intentStates = []; // Protect endpoint const apiKey = props.req.query.apiKey; if (!timeConstantCompare(apiKey, this.webhookSecret)) { return { status: 403, message: 'Invalid Api Key' }; } if (!props.req.body.transaction) { return { status: 200, message: 'Skipping non-transaction webhook' }; } const transaction = props.req.body.transaction; if (transaction.subscription === null) { return { status: 200, message: 'Skipping transaction not related to subscription' }; } const state = mapPayrexxEventToPaymentStatus(transaction.status); if (state !== null && transaction.subscription) { intentStates.push({ paymentID: transaction.referenceId, paymentData: JSON.stringify(transaction), state }); } return { status: 200, paymentStates: intentStates }; }); } // eslint-disable-next-line createIntent(props) { return tslib_1.__awaiter(this, void 0, void 0, function* () { throw new Error('NOT IMPLEMENTED'); }); } // eslint-disable-next-line checkIntentStatus({ intentID }) { return tslib_1.__awaiter(this, void 0, void 0, function* () { throw new Error('NOT IMPLEMENTED'); }); } } exports.PayrexxSubscriptionPaymentProvider = PayrexxSubscriptionPaymentProvider; //# sourceMappingURL=payrexx-subscription-payment-provider.js.map