@liberquack/utils
Version:
Quack utilities
159 lines • 7.74 kB
JavaScript
import { logger } from "../logger.js";
export class AbstractPaymentServer {
providers;
constructor(paymentProviders) {
this.providers = paymentProviders;
}
// async createCard<PN extends ValuesType<PP>["provider"]>(
// user: U,
// providerName: PN,
// card: PaymentAccountProviderData<ValuesType<PP>>
// ): Promise<{ accounts: Array<Extract<PaymentAccountProviderType<ValuesType<PP>>, { provider: PN }> | PaymentUserAccount > }> {
//
// const accountProviders = this.providers.filter(it => "createCard" in it /*TODO: Improve PaymentAccountProvider detection*/) as PaymentAccountProvider[];
// const provider = accountProviders.find(it => it.provider === providerName);
//
// if (!provider) {
// throw `Cannot create card for provider ${providerName}... Expected one of [${accountProviders.map(it => it.provider)}]`
// }
//
// const paymentAccount = await provider.createCard(user, card);
//
// const nextAccounts = this.mergeProviderData(user.payment?.externalProviderAccounts ?? [], paymentAccount);
//
// const paymentData: U["payment"] = {
// ...user.payment,
// accounts: nextAccounts
// };
//
// await this.updateUserPaymentProperties(user, paymentData)
//
// return {accounts: nextAccounts};
// }
// async updateDefaultCard<PN extends ValuesType<PP>["provider"]>(
// user: U,
// providerName: PN,
// card: Parameters<Extract<ValuesType<PP>, PaymentAccountProvider & { provider: PN }>["updateDefaultCard"]>[1]
// ): Promise<{ accounts: PaymentUserAccount[] }> {
// const accountProviders = this.providers.filter(it => "updateDefaultCard" in it) as PaymentAccountProvider[];
// const provider = accountProviders.find(it => it.provider === providerName);
//
// if (!provider) {
// throw `Cannot create card for provider ${providerName}... Expected one of [${accountProviders.map(it => it.provider)}]`
// }
//
// const paymentAccount = await provider.updateDefaultCard(user, card);
//
// const nextAccounts = this.mergeProviderData(user.payment?.externalProviderAccounts ?? [], paymentAccount);
//
// await this.updateUserPaymentProperties(user, {
// ...user.payment,
// accounts: nextAccounts
// })
//
// return {accounts: nextAccounts};
// }
async handleWebhook(providerName, webhookData) {
const provider = this.providers.find(it => it.provider === providerName);
if (!provider)
throw "No payment provider found for handling webhook";
const isSupported = await provider.supportsWebhook(webhookData);
if (!isSupported) {
console.log(this.handleWebhook, `Ignoring webhook from payment provider ${providerName}`);
return;
}
const customerData = await provider.readWebhookCustomer(webhookData);
console.error(this.handleWebhook, { providerName, data: webhookData });
if (!customerData)
throw `Could not read customer id from ${providerName} webhook`;
const user = await this.retrieveUser(customerData);
if (!user)
throw `Could not find user from customer id ${customerData}`;
const checkoutData = await provider.readWebhookCheckout(user, webhookData);
const persistedCheckout = checkoutData && await this.retrieveCheckoutByProviderId(user, checkoutData.providerCheckoutId);
if (!persistedCheckout)
throw `Could not find checkout of webhook from ${user.getId()} provider ${providerName}`;
const checkoutExecution = await provider.handleWebhook(user, persistedCheckout, webhookData);
if (checkoutExecution) {
await this.saveCheckout(user, checkoutExecution);
if (checkoutExecution.success) {
await this.updateUserPaymentProperties(user, checkoutExecution);
}
}
}
/**
* Checkout execution
*
* Keep in mind to watch success field,
*
*
* @param user
* @param checkout
*/
async checkout(user, checkout) {
logger.info("payment-server", `Starting checkout for user ${checkout.userId}, provider ${checkout.provider}, products [${checkout.items.map(it => it.productId)}]`);
const checkoutExecution = await this.providerCheckout(user, checkout);
await this.saveProductsProviderData(checkoutExecution);
await this.saveCheckout(user, checkoutExecution);
if (checkoutExecution.success) {
await this.updateUserPaymentProperties(user, checkoutExecution);
}
return checkoutExecution;
}
async cancelCheckout(user, checkoutId, reason) {
const checkout = await this.retrieveCheckout(user, checkoutId);
if (!checkout.externalData || !checkout.externalId) {
throw "Expected property providerData and externalId. It indicates checkout was not successful, " +
"therefore it's not possible cancel it, please, review checkout id " + checkoutId + " before proceeding";
}
const providerInstance = this.providers.find(it => it.provider === checkout.provider);
if (!providerInstance)
throw `Provider ${checkout.provider} is unavailable, expected providers are ${this.providers.map(it => it.provider)}`;
const checkoutResult = await providerInstance.cancelCheckout(user, checkout);
const savedCheckout = await this.saveCheckout(user, { ...checkoutResult, cancelRequestDate: new Date(), cancelReason: reason });
const paymentData = {
...user.payment,
subscription: checkoutResult.subscription,
lastCheckout: savedCheckout
};
await this.updateUserPaymentProperties(user, paymentData);
return paymentData;
}
async providerCheckout(user, checkout) {
const providerInstance = this.providers.find(it => it.provider === checkout.provider);
if (!providerInstance) {
throw `Server has no payment provider called ${checkout.provider}, possible values are [${this.providers.map(it => it.provider)}]`;
}
const providerData = await providerInstance.checkout(user, checkout);
return providerData;
}
async saveProductsProviderData(checkoutExecution) {
const externalProductData = checkoutExecution.externalProductData;
if (externalProductData) {
for (let extData of externalProductData) {
const product = checkoutExecution.items.find(it => it.productId === extData.productId)?.product;
if (!product) {
logger.warn("payment-server", `Could not update product ${extData.productId} with provider ${checkoutExecution.provider} data`);
continue;
}
const updatedProduct = this.mergeProviderData(product.externalPaymentData ?? [], extData.providerData);
await this.updateProductProvidersData(extData.productId, updatedProduct);
}
}
}
//Builds next providerData array
// if provider data already exists, replace it...
// otherwise add it to the array
mergeProviderData(originalData, data) {
const idx = originalData.findIndex(it => it.provider === data.provider);
let nextData = [...originalData];
if (idx > -1) {
nextData[idx] = data;
}
else {
nextData.push(data);
}
return nextData;
}
}
//# sourceMappingURL=abstract-payment-server.js.map