@wepublish/api
Version:
API core for we.publish.
336 lines • 16.1 kB
JavaScript
;
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