Skip to content

Commit

Permalink
Remove this. state from payment providers
Browse files Browse the repository at this point in the history
  • Loading branch information
wpf500 committed Aug 8, 2024
1 parent 6f7652a commit 3a95321
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 205 deletions.
159 changes: 85 additions & 74 deletions packages/core/src/providers/payment/GCProvider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
PaymentForm,
PaymentMethod,
PaymentSource
} from "@beabee/beabee-common";
import { PaymentForm, PaymentMethod } from "@beabee/beabee-common";
import { Subscription } from "gocardless-nodejs";
import moment from "moment";

Expand All @@ -15,9 +11,7 @@ import gocardless, {
import { log as mainLogger } from "#logging";
import { calcRenewalDate } from "#utils/payment";

import { PaymentProvider } from ".";

import { Contact } from "#models/index";
import { Contact, ContactContribution } from "#models/index";

import { NoPaymentMethod } from "#errors/index";

Expand All @@ -27,56 +21,58 @@ import {
CancelContributionResult,
CompletedPaymentFlow,
ContributionInfo,
UpdateContributionResult
PaymentProvider,
UpdateContributionResult,
UpdatePaymentMethodResult
} from "#type/index";
import { UpdatePaymentMethodResult } from "#type/update-payment-method-result";

const log = mainLogger.child({ app: "gc-payment-provider" });

export default class GCProvider extends PaymentProvider {
async getContributionInfo(): Promise<Partial<ContributionInfo>> {
let paymentSource: PaymentSource | undefined;
let pendingPayment = false;

if (this.data.mandateId) {
try {
const mandate = await gocardless.mandates.get(this.data.mandateId);
class GCProvider implements PaymentProvider {
async getContributionInfo(
contribution: ContactContribution
): Promise<Partial<ContributionInfo>> {
try {
if (contribution.mandateId) {
const mandate = await gocardless.mandates.get(contribution.mandateId);
const bankAccount = await gocardless.customerBankAccounts.get(
mandate.links!.customer_bank_account!
);

paymentSource = {
method: PaymentMethod.GoCardlessDirectDebit,
bankName: bankAccount.bank_name || "",
accountHolderName: bankAccount.account_holder_name || "",
accountNumberEnding: bankAccount.account_number_ending || ""
const pendingPayment = await hasPendingPayment(contribution.mandateId);

return {
paymentSource: {
method: PaymentMethod.GoCardlessDirectDebit,
bankName: bankAccount.bank_name || "",
accountHolderName: bankAccount.account_holder_name || "",
accountNumberEnding: bankAccount.account_number_ending || ""
},
hasPendingPayment: pendingPayment
};
pendingPayment = await hasPendingPayment(this.data.mandateId);
} catch (err: any) {
// 404s can happen on dev as we don't use real mandate IDs
if (!(config.dev && err.response && err.response.status === 404)) {
throw err;
}
}
} catch (err: any) {
// 404s can happen on dev as we don't use real mandate IDs
if (!(config.dev && err.response && err.response.status === 404)) {
throw err;
}
}

return {
hasPendingPayment: pendingPayment,
...(paymentSource && { paymentSource })
};
return {};
}

async canChangeContribution(
contribution: ContactContribution,
useExistingMandate: boolean,
paymentForm: PaymentForm
): Promise<boolean> {
// No payment method available
if (useExistingMandate && !this.data.mandateId) {
if (useExistingMandate && !contribution.mandateId) {
return false;
}

// Can always change contribution if there is no subscription
if (!this.data.subscriptionId) {
if (!contribution.subscriptionId) {
return true;
}

Expand All @@ -85,50 +81,54 @@ export default class GCProvider extends PaymentProvider {
// result in double charging
return (
(useExistingMandate &&
this.data.period === "monthly" &&
contribution.period === "monthly" &&
paymentForm.period === "monthly") ||
!(this.data.mandateId && (await hasPendingPayment(this.data.mandateId)))
!(
contribution.mandateId &&
(await hasPendingPayment(contribution.mandateId))
)
);
}

async updateContribution(
contribution: ContactContribution,
paymentForm: PaymentForm
): Promise<UpdateContributionResult> {
log.info("Update contribution for " + this.contact.id, {
userId: this.contact.id,
log.info("Update contribution for " + contribution.contact.id, {
userId: contribution.contact.id,
paymentForm
});

if (!this.data.mandateId) {
if (!contribution.mandateId) {
throw new NoPaymentMethod();
}

let subscription: Subscription | undefined;

if (this.data.subscriptionId) {
if (contribution.subscriptionId) {
if (
this.contact.membership?.isActive &&
this.data.period === paymentForm.period
contribution.contact.membership?.isActive &&
contribution.period === paymentForm.period
) {
subscription = await updateSubscription(
this.data.subscriptionId,
contribution.subscriptionId,
paymentForm
);
} else {
// Cancel failed subscriptions or when period is changing
await this.cancelContribution(true);
await this.cancelContribution(contribution, true);
}
}

const renewalDate = calcRenewalDate(this.contact);
const renewalDate = calcRenewalDate(contribution.contact);
let expiryDate;

if (subscription) {
expiryDate = subscription.upcoming_payments![0].charge_date;
} else {
log.info("Creating new subscription");
subscription = await createSubscription(
this.data.mandateId,
contribution.mandateId,
paymentForm,
renewalDate
);
Expand All @@ -139,14 +139,14 @@ export default class GCProvider extends PaymentProvider {
const startNow =
!renewalDate ||
(await prorateSubscription(
this.data.mandateId,
contribution.mandateId,
renewalDate,
paymentForm,
this.data.monthlyAmount || 0
contribution.monthlyAmount || 0
));

log.info("Activate contribution for " + this.contact.id, {
userId: this.contact.id,
log.info("Activate contribution for " + contribution.contact.id, {
userId: contribution.contact.id,
paymentForm,
startNow,
expiryDate
Expand All @@ -160,12 +160,15 @@ export default class GCProvider extends PaymentProvider {
}

async cancelContribution(
contribution: ContactContribution,
keepMandate: boolean
): Promise<CancelContributionResult> {
log.info("Cancel subscription for " + this.contact.id, { keepMandate });
log.info("Cancel subscription for " + contribution.contact.id, {
keepMandate
});

const subscriptionId = this.data.subscriptionId;
const mandateId = this.data.mandateId;
const subscriptionId = contribution.subscriptionId;
const mandateId = contribution.mandateId;

if (mandateId && !keepMandate) {
await gocardless.mandates.cancel(mandateId);
Expand All @@ -181,30 +184,31 @@ export default class GCProvider extends PaymentProvider {
}

async updatePaymentMethod(
contribution: ContactContribution,
completedPaymentFlow: CompletedPaymentFlow
): Promise<UpdatePaymentMethodResult> {
log.info("Update payment source for " + this.contact.id, {
userId: this.contact.id,
data: this.data,
log.info("Update payment source for " + contribution.contact.id, {
userId: contribution.contact.id,
data: contribution,
completedPaymentFlow
});

const hadSubscription = !!this.data.subscriptionId;
const hadSubscription = !!contribution.subscriptionId;

// Save before cancelling to stop the webhook triggering a cancelled email
// this.data.subscriptionId = null;
// contribution.subscriptionId = null;
// TODO await this.updateData();

if (this.data.mandateId) {
if (contribution.mandateId) {
// This will also cancel the subscription
await gocardless.mandates.cancel(this.data.mandateId);
await gocardless.mandates.cancel(contribution.mandateId);
}

if (hadSubscription && this.data.period && this.data.monthlyAmount) {
await this.updateContribution({
monthlyAmount: this.data.monthlyAmount,
period: this.data.period,
payFee: !!this.data.payFee,
if (hadSubscription && contribution.period && contribution.monthlyAmount) {
await this.updateContribution(contribution, {
monthlyAmount: contribution.monthlyAmount,
period: contribution.period,
payFee: !!contribution.payFee,
prorate: false
});
}
Expand All @@ -215,25 +219,32 @@ export default class GCProvider extends PaymentProvider {
};
}

async updateContact(updates: Partial<Contact>): Promise<void> {
async updateContact(
contribution: ContactContribution,
updates: Partial<Contact>
): Promise<void> {
if (
(updates.email || updates.firstname || updates.lastname) &&
this.data.customerId
contribution.customerId
) {
log.info("Update contact in GoCardless");
await gocardless.customers.update(this.data.customerId, {
await gocardless.customers.update(contribution.customerId, {
...(updates.email && { email: updates.email }),
...(updates.firstname && { given_name: updates.firstname }),
...(updates.lastname && { family_name: updates.lastname })
});
}
}
async permanentlyDeleteContact(): Promise<void> {
if (this.data.mandateId) {
await gocardless.mandates.cancel(this.data.mandateId);
async permanentlyDeleteContact(
contribution: ContactContribution
): Promise<void> {
if (contribution.mandateId) {
await gocardless.mandates.cancel(contribution.mandateId);
}
if (this.data.customerId) {
await gocardless.customers.remove(this.data.customerId);
if (contribution.customerId) {
await gocardless.customers.remove(contribution.customerId);
}
}
}

export default new GCProvider();
33 changes: 21 additions & 12 deletions packages/core/src/providers/payment/ManualProvider.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import { PaymentForm, PaymentMethod } from "@beabee/beabee-common";

import { Contact } from "#models/index";
import { PaymentProvider } from ".";
import { ContactContribution } from "#models/index";

import {
CancelContributionResult,
CompletedPaymentFlow,
ContributionInfo,
UpdateContributionResult
PaymentProvider,
UpdateContributionResult,
UpdatePaymentMethodResult
} from "#type/index";
import { UpdatePaymentMethodResult } from "#type/update-payment-method-result";

export default class ManualProvider extends PaymentProvider {
async canChangeContribution(useExistingMandate: boolean): Promise<boolean> {
class ManualProvider implements PaymentProvider {
async canChangeContribution(
contribution: ContactContribution,
useExistingMandate: boolean
): Promise<boolean> {
return !useExistingMandate;
}

async getContributionInfo(): Promise<Partial<ContributionInfo>> {
async getContributionInfo(
contribution: ContactContribution
): Promise<Partial<ContributionInfo>> {
return {
paymentSource: {
method: PaymentMethod.Manual,
...(this.data.customerId && {
reference: this.data.customerId
...(contribution.customerId && {
reference: contribution.customerId
}),
...(this.data.mandateId && {
source: this.data.mandateId
...(contribution.mandateId && {
source: contribution.mandateId
})
}
};
Expand All @@ -36,9 +41,10 @@ export default class ManualProvider extends PaymentProvider {
subscriptionId: null
};
}
async updateContact(updates: Partial<Contact>): Promise<void> {}
async updateContact(): Promise<void> {}

async updatePaymentMethod(
contribution: ContactContribution,
flow: CompletedPaymentFlow
): Promise<UpdatePaymentMethodResult> {
return {
Expand All @@ -48,6 +54,7 @@ export default class ManualProvider extends PaymentProvider {
}

async updateContribution(
contribution: ContactContribution,
paymentForm: PaymentForm
): Promise<UpdateContributionResult> {
return {
Expand All @@ -58,3 +65,5 @@ export default class ManualProvider extends PaymentProvider {

async permanentlyDeleteContact(): Promise<void> {}
}

export default new ManualProvider();
16 changes: 10 additions & 6 deletions packages/core/src/providers/payment/NoneProvider.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CancelContributionResult } from "#type/cancel-contribution-results";
import { ContributionInfo } from "#type/contribution-info";
import { UpdateContributionResult } from "#type/update-contribution-result";
import { UpdatePaymentMethodResult } from "#type/update-payment-method-result";
import { PaymentProvider } from ".";
import { ContributionInfo } from "@beabee/beabee-common";
import {
CancelContributionResult,
PaymentProvider,
UpdateContributionResult,
UpdatePaymentMethodResult
} from "#type/index";

export default class NoneProvider extends PaymentProvider {
class NoneProvider implements PaymentProvider {
canChangeContribution(): Promise<boolean> {
throw new Error("Method not implemented.");
}
Expand Down Expand Up @@ -33,3 +35,5 @@ export default class NoneProvider extends PaymentProvider {

async permanentlyDeleteContact(): Promise<void> {}
}

export default new NoneProvider();
Loading

0 comments on commit 3a95321

Please sign in to comment.