diff --git a/src/api/controllers/SignupController.ts b/src/api/controllers/SignupController.ts index 15e603c35..010a73c73 100644 --- a/src/api/controllers/SignupController.ts +++ b/src/api/controllers/SignupController.ts @@ -68,9 +68,9 @@ export class SignupController { throw new NotFoundError(); } - if (data.firstname || data.lastname) { - joinFlow.joinForm.firstname = data.firstname || null; - joinFlow.joinForm.lastname = data.lastname || null; + // Merge additional data into the join form + if (data.firstname || data.lastname || data.vatNumber) { + Object.assign(joinFlow.joinForm, data); await getRepository(JoinFlow).save(joinFlow); } diff --git a/src/api/dto/AddressDto.ts b/src/api/dto/AddressDto.ts index 0d4ba83ed..4eb3da926 100644 --- a/src/api/dto/AddressDto.ts +++ b/src/api/dto/AddressDto.ts @@ -1,6 +1,7 @@ -import Address from "@models/Address"; import { IsDefined, IsOptional, IsString } from "class-validator"; +import { Address } from "@type/address"; + export class UpdateAddressDto implements Address { @IsDefined() @IsString() diff --git a/src/api/dto/SignupFlowDto.ts b/src/api/dto/SignupFlowDto.ts index adda78ab7..e4d5b51cf 100644 --- a/src/api/dto/SignupFlowDto.ts +++ b/src/api/dto/SignupFlowDto.ts @@ -7,11 +7,14 @@ import { IsString } from "class-validator"; +import { UpdateAddressDto } from "@api/dto/AddressDto"; import { StartContributionDto } from "@api/dto/ContributionDto"; import { CompleteJoinFlowDto } from "@api/dto/JoinFlowDto"; import IsPassword from "@api/validators/IsPassword"; import IsUrl from "@api/validators/IsUrl"; +import type JoinForm from "@models/JoinForm"; + import { CompleteUrls } from "@type/complete-urls"; export class StartSignupFlowDto implements CompleteUrls { @@ -37,7 +40,10 @@ export class StartSignupFlowDto implements CompleteUrls { contribution?: StartContributionDto; } -export class CompleteSignupFlowDto extends CompleteJoinFlowDto { +export class CompleteSignupFlowDto + extends CompleteJoinFlowDto + implements Pick +{ @IsOptional() @IsString() firstname?: string; @@ -45,4 +51,8 @@ export class CompleteSignupFlowDto extends CompleteJoinFlowDto { @IsOptional() @IsString() lastname?: string; + + @IsOptional() + @IsString() + vatNumber?: string; } diff --git a/src/api/transformers/AddressTransformer.ts b/src/api/transformers/AddressTransformer.ts index 76829b1d3..e35f979b6 100644 --- a/src/api/transformers/AddressTransformer.ts +++ b/src/api/transformers/AddressTransformer.ts @@ -1,8 +1,8 @@ -import { GetAddressDto } from "@api/dto/AddressDto"; - -import Address from "@models/Address"; import { TransformPlainToInstance } from "class-transformer"; +import { GetAddressDto } from "@api/dto/AddressDto"; +import { Address } from "@type/address"; + // TODO: make Address into a proper model class AddressTransformer { @TransformPlainToInstance(GetAddressDto) diff --git a/src/apps/gift/app.ts b/src/apps/gift/app.ts index dfeda504f..193452337 100644 --- a/src/apps/gift/app.ts +++ b/src/apps/gift/app.ts @@ -11,9 +11,10 @@ import GiftService from "@core/services/GiftService"; import ContactsService from "@core/services/ContactsService"; import OptionsService from "@core/services/OptionsService"; -import Address from "@models/Address"; import GiftFlow, { GiftForm } from "@models/GiftFlow"; +import { Address } from "@type/address"; + import { createGiftSchema, updateGiftAddressSchema } from "./schema.json"; const app = express(); diff --git a/src/apps/tools/apps/exports/exports/GiftsExport.ts b/src/apps/tools/apps/exports/exports/GiftsExport.ts index 1d481cb55..8b3e618e2 100644 --- a/src/apps/tools/apps/exports/exports/GiftsExport.ts +++ b/src/apps/tools/apps/exports/exports/GiftsExport.ts @@ -3,9 +3,10 @@ import { SelectQueryBuilder } from "typeorm"; import { createQueryBuilder } from "@core/database"; -import Address from "@models/Address"; import GiftFlow from "@models/GiftFlow"; +import { Address } from "@type/address"; + import BaseExport, { ExportResult } from "./BaseExport"; function addressFields(address: Address | null) { diff --git a/src/core/providers/payment-flow/GCProvider.ts b/src/core/providers/payment-flow/GCProvider.ts index 87eb1caf5..a4b5851e9 100644 --- a/src/core/providers/payment-flow/GCProvider.ts +++ b/src/core/providers/payment-flow/GCProvider.ts @@ -48,7 +48,7 @@ class GCProvider implements PaymentFlowProvider { log.info("Completed redirect flow " + redirectFlow.id); return { - paymentMethod: joinFlow.joinForm.paymentMethod, + joinForm: joinFlow.joinForm, customerId: redirectFlow.links!.customer!, mandateId: redirectFlow.links!.mandate! }; diff --git a/src/core/providers/payment-flow/StripeProvider.ts b/src/core/providers/payment-flow/StripeProvider.ts index 215338aa3..a07ca32f1 100644 --- a/src/core/providers/payment-flow/StripeProvider.ts +++ b/src/core/providers/payment-flow/StripeProvider.ts @@ -38,12 +38,13 @@ class StripeProvider implements PaymentFlowProvider { log.info("Fetched setup intent " + setupIntent.id); - const paymentMethod = setupIntent.payment_method as string; + const siPaymentMethodId = setupIntent.payment_method as string; return { - paymentMethod: joinFlow.joinForm.paymentMethod, + // paymentMethod: joinFlow.joinForm.paymentMethod, + joinForm: joinFlow.joinForm, customerId: "", // Not needed - mandateId: paymentMethod + mandateId: siPaymentMethodId }; } diff --git a/src/core/providers/payment-flow/index.ts b/src/core/providers/payment-flow/index.ts index 50700d167..b9d76f68a 100644 --- a/src/core/providers/payment-flow/index.ts +++ b/src/core/providers/payment-flow/index.ts @@ -1,7 +1,7 @@ -import { PaymentMethod } from "@beabee/beabee-common"; - -import Address from "@models/Address"; import JoinFlow from "@models/JoinFlow"; +import JoinForm from "@models/JoinForm"; + +import { Address } from "@type/address"; export interface PaymentFlow { id: string; @@ -20,7 +20,7 @@ export interface PaymentFlowData { } export interface CompletedPaymentFlow { - paymentMethod: PaymentMethod; + joinForm: JoinForm; customerId: string; mandateId: string; } diff --git a/src/core/providers/payment/StripeProvider.ts b/src/core/providers/payment/StripeProvider.ts index 82d1f61e9..c24a5a1ea 100644 --- a/src/core/providers/payment/StripeProvider.ts +++ b/src/core/providers/payment/StripeProvider.ts @@ -79,28 +79,47 @@ export default class StripeProvider extends PaymentProvider { await this.updateData(); } - async updatePaymentMethod( - completedPaymentFlow: CompletedPaymentFlow - ): Promise { + async updatePaymentMethod(flow: CompletedPaymentFlow): Promise { + const paymentMethod = await stripe.paymentMethods.retrieve(flow.mandateId); + const address = paymentMethod.billing_details.address; + + const customerData: Stripe.CustomerUpdateParams = { + invoice_settings: { + default_payment_method: flow.mandateId + }, + address: address + ? { + line1: address.line1 || "", + ...(address.city && { city: address.city }), + ...(address.country && { country: address.country }), + ...(address.line2 && { line2: address.line2 }), + ...(address.postal_code && { postal_code: address.postal_code }), + ...(address.state && { state: address.state }) + } + : null + }; + if (this.data.customerId) { log.info("Attach new payment source to " + this.data.customerId); - await stripe.paymentMethods.attach(completedPaymentFlow.mandateId, { + await stripe.paymentMethods.attach(flow.mandateId, { customer: this.data.customerId }); - await stripe.customers.update(this.data.customerId, { - invoice_settings: { - default_payment_method: completedPaymentFlow.mandateId - } - }); + await stripe.customers.update(this.data.customerId, customerData); } else { log.info("Create new customer"); const customer = await stripe.customers.create({ email: this.contact.email, name: `${this.contact.firstname} ${this.contact.lastname}`, - payment_method: completedPaymentFlow.mandateId, - invoice_settings: { - default_payment_method: completedPaymentFlow.mandateId - } + payment_method: flow.mandateId, + ...(flow.joinForm.vatNumber && { + tax_id_data: [ + { + type: "eu_vat", + value: flow.joinForm.vatNumber + } + ] + }), + ...customerData }); this.data.customerId = customer.id; } @@ -110,7 +129,7 @@ export default class StripeProvider extends PaymentProvider { await stripe.paymentMethods.detach(this.data.mandateId); } - this.data.mandateId = completedPaymentFlow.mandateId; + this.data.mandateId = flow.mandateId; await this.updateData(); } diff --git a/src/core/services/GiftService.ts b/src/core/services/GiftService.ts index 132b7ca2f..36f1356be 100644 --- a/src/core/services/GiftService.ts +++ b/src/core/services/GiftService.ts @@ -1,22 +1,23 @@ import { ContributionType, NewsletterStatus } from "@beabee/beabee-common"; import muhammara from "muhammara"; import moment from "moment"; -import { getRepository } from "typeorm"; +import { getRepository } from "@core/database"; import { log as mainLogger } from "@core/logging"; import stripe from "@core/lib/stripe"; import { isDuplicateIndex } from "@core/utils"; +import { generateContactCode } from "@core/utils/contact"; import EmailService from "@core/services/EmailService"; import ContactsService from "@core/services/ContactsService"; import OptionsService from "@core/services/OptionsService"; -import Address from "@models/Address"; import GiftFlow, { GiftForm } from "@models/GiftFlow"; import ContactRole from "@models/ContactRole"; import config from "@config"; -import { generateContactCode } from "@core/utils/contact"; + +import { Address } from "@type/address"; const log = mainLogger.child({ app: "gift-service" }); diff --git a/src/core/services/PaymentFlowService.ts b/src/core/services/PaymentFlowService.ts index 0963432e4..49e924774 100644 --- a/src/core/services/PaymentFlowService.ts +++ b/src/core/services/PaymentFlowService.ts @@ -8,8 +8,6 @@ import ContactsService from "@core/services/ContactsService"; import OptionsService from "@core/services/OptionsService"; import PaymentService from "@core/services/PaymentService"; import ResetSecurityFlowService from "./ResetSecurityFlowService"; - -import Address from "@models/Address"; import JoinFlow from "@models/JoinFlow"; import JoinForm from "@models/JoinForm"; import Contact from "@models/Contact"; @@ -29,6 +27,7 @@ import DuplicateEmailError from "@api/errors/DuplicateEmailError"; import { RESET_SECURITY_FLOW_TYPE } from "@enums/reset-security-flow-type"; +import { Address } from "@type/address"; import { CompleteUrls } from "@type/complete-urls"; const paymentProviders = { @@ -225,7 +224,7 @@ class PaymentFlowService implements PaymentFlowProvider { completedPaymentFlow: CompletedPaymentFlow ): Promise { return paymentProviders[ - completedPaymentFlow.paymentMethod + completedPaymentFlow.joinForm.paymentMethod ].getCompletedPaymentFlowData(completedPaymentFlow); } } diff --git a/src/core/services/PaymentService.ts b/src/core/services/PaymentService.ts index aaf9c1706..60cf97dea 100644 --- a/src/core/services/PaymentService.ts +++ b/src/core/services/PaymentService.ts @@ -177,7 +177,7 @@ class PaymentService { }); const data = await this.getData(contact); - const newMethod = completedPaymentFlow.paymentMethod; + const newMethod = completedPaymentFlow.joinForm.paymentMethod; if (data.method !== newMethod) { log.info( "Changing payment method, cancelling any previous contribution", diff --git a/src/migrations/1712158796671-AddExtraJoinFormFields.ts b/src/migrations/1712158796671-AddExtraJoinFormFields.ts new file mode 100644 index 000000000..d554eddaf --- /dev/null +++ b/src/migrations/1712158796671-AddExtraJoinFormFields.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddExtraJoinFormFields1712158796671 implements MigrationInterface { + name = "AddExtraJoinFormFields1712158796671"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "join_flow" ADD "joinFormBillingaddress" jsonb` + ); + await queryRunner.query( + `ALTER TABLE "join_flow" ADD "joinFormVatnumber" character varying` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "join_flow" DROP COLUMN "joinFormVatnumber"` + ); + await queryRunner.query( + `ALTER TABLE "join_flow" DROP COLUMN "joinFormBillingaddress"` + ); + } +} diff --git a/src/migrations/1712163090314-RemoveJoinFormBillingAddress.ts b/src/migrations/1712163090314-RemoveJoinFormBillingAddress.ts new file mode 100644 index 000000000..0039f2eee --- /dev/null +++ b/src/migrations/1712163090314-RemoveJoinFormBillingAddress.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class RemoveJoinFormBillingAddress1712163090314 + implements MigrationInterface +{ + name = "RemoveJoinFormBillingAddress1712163090314"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "join_flow" DROP COLUMN "joinFormBillingaddress"` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "join_flow" ADD "joinFormBillingaddress" jsonb` + ); + } +} diff --git a/src/models/ContactProfile.ts b/src/models/ContactProfile.ts index ad52af0da..7c3a68951 100644 --- a/src/models/ContactProfile.ts +++ b/src/models/ContactProfile.ts @@ -1,9 +1,10 @@ import { NewsletterStatus } from "@beabee/beabee-common"; import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from "typeorm"; -import type Address from "./Address"; import type Contact from "./Contact"; +import { Address } from "@type/address"; + @Entity() export default class ContactProfile { @PrimaryColumn() diff --git a/src/models/GiftFlow.ts b/src/models/GiftFlow.ts index 654a1a78b..36aa94099 100644 --- a/src/models/GiftFlow.ts +++ b/src/models/GiftFlow.ts @@ -6,9 +6,10 @@ import { PrimaryGeneratedColumn } from "typeorm"; -import type Address from "./Address"; import type Contact from "./Contact"; +import { Address } from "@type/address"; + export class GiftForm { @Column() firstname!: string; diff --git a/src/models/JoinForm.ts b/src/models/JoinForm.ts index 6e3d05e44..fabdae268 100644 --- a/src/models/JoinForm.ts +++ b/src/models/JoinForm.ts @@ -1,8 +1,11 @@ import { ContributionPeriod, PaymentMethod } from "@beabee/beabee-common"; import { Column } from "typeorm"; + import { PaymentForm } from "@core/utils"; import Password from "./Password"; +import { Address } from "@type/address"; + export interface ReferralGiftForm { referralGift?: string | null; referralGiftOptions?: Record | null; @@ -15,12 +18,6 @@ export default class JoinForm implements PaymentForm, ReferralGiftForm { @Column(() => Password) password!: Password; - @Column({ type: String, nullable: true }) - firstname?: string | null; - - @Column({ type: String, nullable: true }) - lastname?: string | null; - @Column({ type: "real" }) monthlyAmount!: number; @@ -36,6 +33,15 @@ export default class JoinForm implements PaymentForm, ReferralGiftForm { @Column() paymentMethod!: PaymentMethod; + @Column({ type: String, nullable: true }) + firstname?: string | null; + + @Column({ type: String, nullable: true }) + lastname?: string | null; + + @Column({ type: String, nullable: true }) + vatNumber?: string | null; + @Column({ type: String, nullable: true }) referralCode?: string | null; diff --git a/src/tools/database/import-steady.ts b/src/tools/database/import-steady.ts index 563d8f1ec..b1330fe2c 100644 --- a/src/tools/database/import-steady.ts +++ b/src/tools/database/import-steady.ts @@ -14,10 +14,11 @@ import { cleanEmailAddress } from "@core/utils"; import ContactsService from "@core/services/ContactsService"; -import Address from "@models/Address"; import Contact from "@models/Contact"; import ContactRole from "@models/ContactRole"; +import { Address } from "@type/address"; + const headers = [ "first_name", "last_name", diff --git a/src/models/Address.ts b/src/type/address.ts similarity index 70% rename from src/models/Address.ts rename to src/type/address.ts index b9c0b30c2..44c1e1bea 100644 --- a/src/models/Address.ts +++ b/src/type/address.ts @@ -1,4 +1,4 @@ -export default interface Address { +export interface Address { line1: string; line2?: string | undefined; city: string;