From 9d08ddd7f49bf65c7bfad70dec7ac2df098cc771 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Wed, 15 May 2024 18:14:27 +0100 Subject: [PATCH] Start moving contribution info from Contact into ContactContribution --- src/api/transformers/ContactExporter.ts | 6 +- src/api/transformers/ContactTransformer.ts | 10 ++- .../apps/contribution/views/automatic.pug | 6 +- .../contribution/views/partials/fields.pug | 4 +- .../member/views/partials/contribution.pug | 2 +- src/apps/members/views/partials/table.pug | 4 +- .../exports/exports/ActiveMembersExport.ts | 4 +- src/core/providers/payment/GCProvider.ts | 23 +++--- src/core/services/NewsletterService.ts | 16 +++- src/core/services/PaymentService.ts | 78 ++++++++++--------- src/models/ContactContribution.ts | 43 +++++++++- src/tools/gocardless/migrate-to-stripe.ts | 9 +-- src/tools/test-users.ts | 12 ++- 13 files changed, 134 insertions(+), 83 deletions(-) diff --git a/src/api/transformers/ContactExporter.ts b/src/api/transformers/ContactExporter.ts index bb6028bb4..468029928 100644 --- a/src/api/transformers/ContactExporter.ts +++ b/src/api/transformers/ContactExporter.ts @@ -27,9 +27,9 @@ class ContactExporter extends BaseContactTransformer< Joined: contact.joined.toISOString(), Tags: contact.profile.tags.join(", "), ContributionType: contact.contributionType, - ContributionMonthlyAmount: contact.contributionMonthlyAmount, - ContributionPeriod: contact.contributionPeriod, - ContributionDescription: contact.contributionDescription, + ContributionMonthlyAmount: contact.contribution.monthlyAmount, + ContributionPeriod: contact.contribution.period, + ContributionDescription: contact.contribution.description, ContributionCancelled: contact.contribution.cancelledAt?.toISOString() || "", MembershipStarts: contact.membership?.dateAdded.toISOString() || "", diff --git a/src/api/transformers/ContactTransformer.ts b/src/api/transformers/ContactTransformer.ts index 37dbe225e..0a7831f7b 100644 --- a/src/api/transformers/ContactTransformer.ts +++ b/src/api/transformers/ContactTransformer.ts @@ -40,11 +40,11 @@ class ContactTransformer extends BaseContactTransformer< ...(contact.lastSeen && { lastSeen: contact.lastSeen }), - ...(contact.contributionAmount && { - contributionAmount: contact.contributionAmount + ...(contact.contribution.amount !== null && { + contributionAmount: contact.contribution.amount }), - ...(contact.contributionPeriod && { - contributionPeriod: contact.contributionPeriod + ...(contact.contribution.period && { + contributionPeriod: contact.contribution.period }), ...(opts?.with?.includes(GetContactWith.Profile) && contact.profile && { @@ -86,6 +86,8 @@ class ContactTransformer extends BaseContactTransformer< query: ListContactsDto ): void { { + qb.innerJoinAndSelect(`${fieldPrefix}contribution`, "contribution"); + if (query.with?.includes(GetContactWith.Profile)) { qb.innerJoinAndSelect(`${fieldPrefix}profile`, "profile"); } diff --git a/src/apps/members/apps/member/apps/contribution/views/automatic.pug b/src/apps/members/apps/member/apps/contribution/views/automatic.pug index 560b92b0c..c80714baa 100644 --- a/src/apps/members/apps/member/apps/contribution/views/automatic.pug +++ b/src/apps/members/apps/member/apps/contribution/views/automatic.pug @@ -26,15 +26,15 @@ block contents value: member.contributionMonthlyAmount }) if !!contribution.subscriptionId - input(type='hidden' name='period' value=member.contributionPeriod) + input(type='hidden' name='period' value=contribution.period) .form-group label.col-md-3.control-label Period - .col-md-4.form-control-static= member.contributionPeriod + .col-md-4.form-control-static= contribution.period else +input( 'radio', 'Period', 'period', { left: 3, right: 4, options: {'monthly': 'Monthly', 'annually': 'Annually'}, - value: member.contributionPeriod + value: contribution.period }) +input( 'checkbox', 'Paying fee', 'payFee', { left: 3, right: 4, diff --git a/src/apps/members/apps/member/apps/contribution/views/partials/fields.pug b/src/apps/members/apps/member/apps/contribution/views/partials/fields.pug index c5b68d058..1e1ca8f0d 100644 --- a/src/apps/members/apps/member/apps/contribution/views/partials/fields.pug +++ b/src/apps/members/apps/member/apps/contribution/views/partials/fields.pug @@ -14,12 +14,12 @@ .js-reveal-type(data-type='Manual') +input( 'number', 'Amount', 'amount', { left: 3, right: 4, before: currencySymbol, - value: member.contributionAmount + value: contribution.amount }) +input( 'radio', 'Period', 'period', { left: 3, right: 4, options: {'monthly': 'Monthly', 'annually': 'Annually'}, - value: member.contributionPeriod + value: contribution.period }) .form-group diff --git a/src/apps/members/apps/member/views/partials/contribution.pug b/src/apps/members/apps/member/views/partials/contribution.pug index 8eb88d585..ac9811148 100644 --- a/src/apps/members/apps/member/views/partials/contribution.pug +++ b/src/apps/members/apps/member/views/partials/contribution.pug @@ -5,7 +5,7 @@ dl.dl-horizontal if member.contributionType dt Amount dd - = member.contributionDescription + = contribution.description case member.contributionType when 'Automatic' include automatic.pug diff --git a/src/apps/members/views/partials/table.pug b/src/apps/members/views/partials/table.pug index 465e4b02a..0453dc1a9 100644 --- a/src/apps/members/views/partials/table.pug +++ b/src/apps/members/views/partials/table.pug @@ -9,9 +9,9 @@ mixin contactsTable +membersTableBasicInfo(contact) span(style='flex: 0 1 120px') = currencySymbol - = contact.contributionAmount + = contact.contribution.amount | / - = contact.contributionPeriod + = contact.contribution.period if addToProject form(method='POST' action='/projects/' + addToProject.id) diff --git a/src/apps/tools/apps/exports/exports/ActiveMembersExport.ts b/src/apps/tools/apps/exports/exports/ActiveMembersExport.ts index 4b2084d07..ae3b7c36a 100644 --- a/src/apps/tools/apps/exports/exports/ActiveMembersExport.ts +++ b/src/apps/tools/apps/exports/exports/ActiveMembersExport.ts @@ -62,8 +62,8 @@ export default class ActiveMembersExport extends BaseExport { PollsCode: contact.pollsCode, ContributionType: contact.contributionType, ContributionMonthlyAmount: contact.contributionMonthlyAmount, - ContributionPeriod: contact.contributionPeriod, - ContributionDescription: contact.contributionDescription + ContributionPeriod: contact.contribution.period, + ContributionDescription: contact.contribution.description })); } } diff --git a/src/core/providers/payment/GCProvider.ts b/src/core/providers/payment/GCProvider.ts index e2963f965..b8c2658e0 100644 --- a/src/core/providers/payment/GCProvider.ts +++ b/src/core/providers/payment/GCProvider.ts @@ -1,4 +1,8 @@ -import { PaymentMethod, PaymentSource } from "@beabee/beabee-common"; +import { + ContributionPeriod, + PaymentMethod, + PaymentSource +} from "@beabee/beabee-common"; import { Subscription } from "gocardless-nodejs"; import moment from "moment"; @@ -79,8 +83,8 @@ export default class GCProvider extends PaymentProvider { // result in double charging return ( (useExistingMandate && - this.contact.contributionPeriod === "monthly" && - paymentForm.period === "monthly") || + this.data.period === ContributionPeriod.Monthly && + paymentForm.period === ContributionPeriod.Monthly) || !(this.data.mandateId && (await hasPendingPayment(this.data.mandateId))) ); } @@ -102,7 +106,7 @@ export default class GCProvider extends PaymentProvider { if (this.data.subscriptionId) { if ( this.contact.membership?.isActive && - this.contact.contributionPeriod === paymentForm.period + this.data.period === paymentForm.period ) { subscription = await updateSubscription( this.data.subscriptionId, @@ -206,14 +210,11 @@ export default class GCProvider extends PaymentProvider { await gocardless.mandates.cancel(mandateId); } - if ( - hadSubscription && - this.contact.contributionPeriod && - this.contact.contributionMonthlyAmount - ) { + // Recreate the subscription if the user had one + if (hadSubscription && this.data.period && this.data.monthlyAmount) { await this.updateContribution({ - monthlyAmount: this.contact.contributionMonthlyAmount, - period: this.contact.contributionPeriod, + monthlyAmount: this.data.monthlyAmount, + period: this.data.period, payFee: !!this.data.payFee, prorate: false }); diff --git a/src/core/services/NewsletterService.ts b/src/core/services/NewsletterService.ts index a2c1acf02..b589c5ce2 100644 --- a/src/core/services/NewsletterService.ts +++ b/src/core/services/NewsletterService.ts @@ -12,6 +12,7 @@ import MailchimpProvider from "@core/providers/newsletter/MailchimpProvider"; import NoneProvider from "@core/providers/newsletter/NoneProvider"; import Contact from "@models/Contact"; +import ContactContribution from "@models/ContactContribution"; import ContactProfile from "@models/ContactProfile"; import config from "@config"; @@ -40,6 +41,15 @@ async function contactToNlUpdate( }); } + // TODO: Fix that it relies on contact.contribution being loaded + if (!contact.contribution) { + contact.contribution = await getRepository( + ContactContribution + ).findOneByOrFail({ + contactId: contact.id + }); + } + if (contact.profile.newsletterStatus !== NewsletterStatus.None) { return { email: contact.email, @@ -50,9 +60,9 @@ async function contactToNlUpdate( fields: { REFCODE: contact.referralCode || "", POLLSCODE: contact.pollsCode || "", - C_DESC: contact.contributionDescription, - C_MNTHAMT: contact.contributionMonthlyAmount?.toFixed(2) || "", - C_PERIOD: contact.contributionPeriod || "" + C_DESC: contact.contribution.description, + C_MNTHAMT: contact.contribution.monthlyAmount?.toFixed(2) || "", + C_PERIOD: contact.contribution.period || "" } }; } diff --git a/src/core/services/PaymentService.ts b/src/core/services/PaymentService.ts index 3e40a1a5c..3a049696e 100644 --- a/src/core/services/PaymentService.ts +++ b/src/core/services/PaymentService.ts @@ -52,8 +52,8 @@ class PaymentService { ).findOneByOrFail({ contactId: contact.id }); - // No need to refetch contact, just add it in - return { ...contribution, contact }; + contribution.contact = contact; // No need to refetch contact, just add it in + return contribution; } async getContributionBy( @@ -101,40 +101,46 @@ class PaymentService { } async getContributionInfo(contact: Contact): Promise { - return await this.provider(contact, async (p, d) => { - // Store payment data in contact for getMembershipStatus - // TODO: fix this! - contact.contribution = d; - - const renewalDate = !d.cancelledAt && calcRenewalDate(contact); - - return { - type: contact.contributionType, - ...(contact.contributionAmount !== null && { - amount: contact.contributionAmount - }), - ...(contact.contributionPeriod !== null && { - period: contact.contributionPeriod - }), - ...(d.payFee !== null && { - payFee: d.payFee - }), - ...(d.nextAmount && - contact.contributionPeriod && { - nextAmount: getActualAmount( - d.nextAmount.monthly, - contact.contributionPeriod - ) + return await this.provider( + contact, + async (provider, contribution) => { + // Store contribution in contact for getMembershipStatus + // TODO: fix this! + contact.contribution = contribution; + + const renewalDate = + !contribution.cancelledAt && calcRenewalDate(contact); + + return { + type: contact.contributionType, + ...(contribution.amount !== null && { + amount: contribution.amount }), - ...(contact.membership?.dateExpires && { - membershipExpiryDate: contact.membership.dateExpires - }), - membershipStatus: getMembershipStatus(contact), - ...(await p.getContributionInfo()), - ...(d.cancelledAt && { cancellationDate: d.cancelledAt }), - ...(renewalDate && { renewalDate }) - }; - }); + ...(contribution.period !== null && { + period: contribution.period + }), + ...(contribution.payFee !== null && { + payFee: contribution.payFee + }), + ...(contribution.nextAmount && + contribution.period && { + nextAmount: getActualAmount( + contribution.nextAmount.monthly, + contribution.period + ) + }), + ...(contribution.cancelledAt && { + cancellationDate: contribution.cancelledAt + }), + ...(contact.membership?.dateExpires && { + membershipExpiryDate: contact.membership.dateExpires + }), + membershipStatus: getMembershipStatus(contact), + ...(await provider.getContributionInfo()), + ...(renewalDate && { renewalDate }) + }; + } + ); } async getPayments(contact: Contact): Promise { @@ -190,7 +196,7 @@ class PaymentService { // Clear the old payment data, set the new method Object.assign(contribution, { - ...ContactContribution.empty, + ...ContactContribution.none, method: newMethod }); await getRepository(ContactContribution).save(contribution); diff --git a/src/models/ContactContribution.ts b/src/models/ContactContribution.ts index 233d52b5a..3db254a9b 100644 --- a/src/models/ContactContribution.ts +++ b/src/models/ContactContribution.ts @@ -1,8 +1,12 @@ -import { PaymentMethod } from "@beabee/beabee-common"; +import { ContributionPeriod, PaymentMethod } from "@beabee/beabee-common"; import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from "typeorm"; +import { getActualAmount } from "@core/utils"; + import type Contact from "./Contact"; +import config from "@config"; + @Entity() export default class ContactContribution { @PrimaryColumn() @@ -14,6 +18,12 @@ export default class ContactContribution { @Column({ type: String, nullable: true }) method!: PaymentMethod | null; + @Column({ type: Number, nullable: true }) + monthlyAmount!: number | null; + + @Column({ type: String, nullable: true }) + period!: ContributionPeriod | null; + @Column({ type: String, nullable: true }) customerId!: string | null; @@ -32,15 +42,42 @@ export default class ContactContribution { @Column({ type: Date, nullable: true }) cancelledAt!: Date | null; - static get empty(): Omit { + get amount(): number | null { + return this.monthlyAmount === null || this.period === null + ? null + : getActualAmount(this.monthlyAmount, this.period); + } + + get description(): string { + /*if (this.contributionType === "Gift") { + return "Gift"; + } else */ if ( + this.method === null || + this.period === null || + this.amount === null + ) { + return "None"; + } else { + return `${config.currencySymbol}${this.amount}/${ + this.period === "monthly" ? "month" : "year" + }`; + } + } + + static get none(): Omit { return { method: null, + monthlyAmount: null, + amount: null, customerId: null, mandateId: null, subscriptionId: null, payFee: null, nextAmount: null, - cancelledAt: null + cancelledAt: null, + + period: null, // TODO + description: "None" }; } } diff --git a/src/tools/gocardless/migrate-to-stripe.ts b/src/tools/gocardless/migrate-to-stripe.ts index de99cd197..cf5ba4594 100644 --- a/src/tools/gocardless/migrate-to-stripe.ts +++ b/src/tools/gocardless/migrate-to-stripe.ts @@ -120,10 +120,7 @@ runApp(async () => { contact.email, contactPayments.length ); - } else if ( - !contact.contributionPeriod || - !contact.contributionMonthlyAmount - ) { + } else if (!contribution.period || !contribution.monthlyAmount) { console.error( "ERROR: Contact doesn't have a contribution amount or period" ); @@ -161,8 +158,8 @@ runApp(async () => { // Recreate the contribution await PaymentService.updateContribution(contact, { - monthlyAmount: contact.contributionMonthlyAmount, - period: contact.contributionPeriod, + monthlyAmount: contribution.monthlyAmount, + period: contribution.period, payFee: false, prorate: false }); diff --git a/src/tools/test-users.ts b/src/tools/test-users.ts index a31c27153..cd2946bf8 100644 --- a/src/tools/test-users.ts +++ b/src/tools/test-users.ts @@ -21,6 +21,7 @@ import ContactContribution from "@models/ContactContribution"; async function logContact(type: string, conditions: Brackets[]) { const qb = createQueryBuilder(Contact, "m") .innerJoinAndSelect("m.roles", "mp") + .innerJoinAndSelect("m.contribution", "cc") .where("TRUE"); for (const condition of conditions) { @@ -53,13 +54,10 @@ async function logContactVaryContributions( [ ...conditions, new Brackets((qb) => - qb.where( - "m.contributionMonthlyAmount = :amount AND m.contributionPeriod = :period", - { - amount, - period - } - ) + qb.where("cc.monthlyAmount = :amount AND cc.period = :period", { + amount, + period + }) ) ] );