From c90fbeb815151bf40e245eac86bc50c257cd32a3 Mon Sep 17 00:00:00 2001 From: julianpoyourow Date: Mon, 23 Sep 2024 18:15:22 +0000 Subject: [PATCH] feat(payments-cart): add currency to cart Because: - We want to determine currency based on the tax address provided by the customer This commit: - Automatically determines the tax address and currency based on their IP address - Allows user to update their tax address, thereby updating their currency Closes FXA-7584 --- apps/payments/next/.env.development | 4 + apps/payments/next/.env.production | 4 + .../[interval]/[cartId]/start/page.tsx | 1 - libs/payments/cart/src/lib/cart.error.ts | 14 +++ libs/payments/cart/src/lib/cart.factories.ts | 1 + libs/payments/cart/src/lib/cart.manager.ts | 2 + .../cart/src/lib/cart.service.spec.ts | 93 +++++++++++++++++-- libs/payments/cart/src/lib/cart.service.ts | 32 ++++++- libs/payments/cart/src/lib/cart.types.ts | 2 + .../cart/src/lib/checkout.service.spec.ts | 13 +++ .../payments/cart/src/lib/checkout.service.ts | 8 ++ .../currency/src/lib/currency.config.ts | 8 ++ .../currency/src/lib/currency.manager.ts | 12 +++ .../metrics/src/lib/glean/glean.types.ts | 7 +- .../payments/ui/src/lib/nestapp/app.module.ts | 2 + libs/payments/ui/src/lib/nestapp/config.ts | 6 ++ .../db/mysql/account/src/lib/factories.ts | 1 + .../db/mysql/account/src/lib/kysely-types.ts | 1 + .../db/mysql/account/src/test/carts.sql | 1 + .../databases/fxa/patches/patch-154-155.sql | 6 ++ .../databases/fxa/patches/patch-155-154.sql | 5 + .../databases/fxa/target-patch.json | 2 +- 22 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 packages/db-migrations/databases/fxa/patches/patch-154-155.sql create mode 100644 packages/db-migrations/databases/fxa/patches/patch-155-154.sql diff --git a/apps/payments/next/.env.development b/apps/payments/next/.env.development index 5f28eb8498..d3f9f3715c 100644 --- a/apps/payments/next/.env.development +++ b/apps/payments/next/.env.development @@ -68,6 +68,10 @@ FIRESTORE_CONFIG__CREDENTIALS__PRIVATE_KEY= FIRESTORE_CONFIG__KEY_FILENAME= FIRESTORE_CONFIG__PROJECT_ID= +# Currency Config +CURRENCY_CONFIG__TAX_IDS={ "EUR": "EU1234", "CHF": "CH1234" } +CURRENCY_CONFIG__CURRENCIES_TO_COUNTRIES={ "USD": ["US", "GB", "NZ", "MY", "SG", "CA", "AS", "GU", "MP", "PR", "VI"], "EUR": ["FR", "DE"] } + # StatsD Config STATS_D_CONFIG__SAMPLE_RATE= STATS_D_CONFIG__MAX_BUFFER_SIZE= diff --git a/apps/payments/next/.env.production b/apps/payments/next/.env.production index d35ec526a4..d70fdd8526 100644 --- a/apps/payments/next/.env.production +++ b/apps/payments/next/.env.production @@ -64,6 +64,10 @@ FIRESTORE_CONFIG__CREDENTIALS__PRIVATE_KEY= FIRESTORE_CONFIG__KEY_FILENAME= FIRESTORE_CONFIG__PROJECT_ID= +# Currency Config +CURRENCY_CONFIG__TAX_IDS={} +CURRENCY_CONFIG__CURRENCIES_TO_COUNTRIES={} + # StatsD Config STATS_D_CONFIG__SAMPLE_RATE= STATS_D_CONFIG__MAX_BUFFER_SIZE= diff --git a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/start/page.tsx b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/start/page.tsx index 927f059cc9..d3b8933a1e 100644 --- a/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/start/page.tsx +++ b/apps/payments/next/app/[locale]/[offeringId]/checkout/[interval]/[cartId]/start/page.tsx @@ -54,7 +54,6 @@ export default async function Checkout({ getApp() .getGleanEmitter() .emit('fxaPaySetupView', { - currency: 'USD', checkoutType: 'without-accounts', params: { ...params }, searchParams, diff --git a/libs/payments/cart/src/lib/cart.error.ts b/libs/payments/cart/src/lib/cart.error.ts index c5f6b91a27..860bb0ea4e 100644 --- a/libs/payments/cart/src/lib/cart.error.ts +++ b/libs/payments/cart/src/lib/cart.error.ts @@ -119,3 +119,17 @@ export class CartInvalidPromoCodeError extends CartError { }); } } + +export class CartInvalidCurrencyError extends CartError { + constructor( + currency: string | undefined, + country: string | undefined, + cartId?: string + ) { + super('Cart specified currency is not supported', { + cartId, + currency, + country, + }); + } +} diff --git a/libs/payments/cart/src/lib/cart.factories.ts b/libs/payments/cart/src/lib/cart.factories.ts index d0563b880c..fdbcd0cd0f 100644 --- a/libs/payments/cart/src/lib/cart.factories.ts +++ b/libs/payments/cart/src/lib/cart.factories.ts @@ -84,6 +84,7 @@ export const ResultCartFactory = ( interval: faker.string.numeric(), experiment: null, taxAddress: TaxAddressFactory(), + currency: faker.finance.currencyCode(), createdAt: faker.date.past().getTime(), updatedAt: faker.date.past().getTime(), couponCode: null, diff --git a/libs/payments/cart/src/lib/cart.manager.ts b/libs/payments/cart/src/lib/cart.manager.ts index b41c669f6a..04f0043a16 100644 --- a/libs/payments/cart/src/lib/cart.manager.ts +++ b/libs/payments/cart/src/lib/cart.manager.ts @@ -89,6 +89,7 @@ export class CartManager { taxAddress: input.taxAddress ? JSON.stringify(input.taxAddress) : undefined, + currency: input.currency, id: uuidv4({}, Buffer.alloc(16)), uid: input.uid ? Buffer.from(input.uid, 'hex') : undefined, state: CartState.START, @@ -136,6 +137,7 @@ export class CartManager { taxAddress: items.taxAddress ? JSON.stringify(items.taxAddress) : undefined, + currency: items.currency, uid: items.uid ? Buffer.from(items.uid, 'hex') : undefined, }); } catch (error) { diff --git a/libs/payments/cart/src/lib/cart.service.spec.ts b/libs/payments/cart/src/lib/cart.service.spec.ts index 76d2876c65..c601309418 100644 --- a/libs/payments/cart/src/lib/cart.service.spec.ts +++ b/libs/payments/cart/src/lib/cart.service.spec.ts @@ -67,7 +67,12 @@ import { import { CartManager } from './cart.manager'; import { CartService } from './cart.service'; import { CheckoutService } from './checkout.service'; -import { CartInvalidPromoCodeError } from './cart.error'; +import { + CartInvalidCurrencyError, + CartInvalidPromoCodeError, +} from './cart.error'; +import { CurrencyManager } from '@fxa/payments/currency'; +import { MockCurrencyConfigProvider } from 'libs/payments/currency/src/lib/currency.config'; describe('CartService', () => { let accountCustomerManager: AccountCustomerManager; @@ -75,6 +80,7 @@ describe('CartService', () => { let cartManager: CartManager; let checkoutService: CheckoutService; let customerManager: CustomerManager; + let currencyManager: CurrencyManager; let promotionCodeManager: PromotionCodeManager; let eligibilityService: EligibilityService; let geodbManager: GeoDBManager; @@ -114,6 +120,8 @@ describe('CartService', () => { StrapiClient, StripeClient, SubscriptionManager, + CurrencyManager, + MockCurrencyConfigProvider, ], }).compile(); @@ -122,6 +130,7 @@ describe('CartService', () => { cartService = moduleRef.get(CartService); checkoutService = moduleRef.get(CheckoutService); customerManager = moduleRef.get(CustomerManager); + currencyManager = moduleRef.get(CurrencyManager); promotionCodeManager = moduleRef.get(PromotionCodeManager); eligibilityService = moduleRef.get(EligibilityService); geodbManager = moduleRef.get(GeoDBManager); @@ -147,6 +156,7 @@ describe('CartService', () => { const taxAddress = TaxAddressFactory(); const mockPrice = StripePriceFactory(); const mockInvoicePreview = InvoicePreviewFactory(); + const mockResolvedCurrency = faker.finance.currencyCode(); jest .spyOn(eligibilityService, 'checkEligibility') @@ -165,6 +175,9 @@ describe('CartService', () => { jest .spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice') .mockResolvedValue(undefined); + jest + .spyOn(currencyManager, 'getCurrencyForCountry') + .mockReturnValue(mockResolvedCurrency); jest.spyOn(cartManager, 'createCart').mockResolvedValue(mockResultCart); const result = await cartService.setupCart(args); @@ -177,6 +190,7 @@ describe('CartService', () => { stripeCustomerId: mockAccountCustomer.stripeCustomerId, experiment: args.experiment, taxAddress, + currency: mockResolvedCurrency, eligibilityStatus: CartEligibilityStatus.CREATE, }); expect(result).toEqual(mockResultCart); @@ -225,6 +239,56 @@ describe('CartService', () => { expect(cartManager.createCart).not.toHaveBeenCalled(); }); + + it('throws an error when country to currency result is invalid', async () => { + const mockCustomer = StripeResponseFactory(StripeCustomerFactory()); + const mockAccountCustomer = ResultAccountCustomerFactory({ + stripeCustomerId: mockCustomer.id, + }); + const mockResultCart = ResultCartFactory(); + const args = { + interval: SubplatInterval.Monthly, + offeringConfigId: faker.string.uuid(), + experiment: faker.string.uuid(), + promoCode: faker.word.noun(), + uid: faker.string.uuid(), + ip: faker.internet.ipv4(), + }; + const taxAddress = TaxAddressFactory(); + const mockPrice = StripePriceFactory(); + const mockInvoicePreview = InvoicePreviewFactory(); + + jest + .spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice') + .mockRejectedValue(undefined); + jest + .spyOn(eligibilityService, 'checkEligibility') + .mockResolvedValue(EligibilityStatus.CREATE); + jest.spyOn(geodbManager, 'getTaxAddress').mockReturnValue(taxAddress); + jest + .spyOn(accountCustomerManager, 'getAccountCustomerByUid') + .mockResolvedValue(mockAccountCustomer); + jest + .spyOn(productConfigurationManager, 'retrieveStripePrice') + .mockResolvedValue(mockPrice); + jest.spyOn(customerManager, 'retrieve').mockResolvedValue(mockCustomer); + jest + .spyOn(invoiceManager, 'preview') + .mockResolvedValue(mockInvoicePreview); + jest + .spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice') + .mockResolvedValue(undefined); + jest + .spyOn(currencyManager, 'getCurrencyForCountry') + .mockReturnValue(undefined); + jest.spyOn(cartManager, 'createCart').mockResolvedValue(mockResultCart); + + await expect(() => cartService.setupCart(args)).rejects.toThrowError( + CartInvalidCurrencyError + ); + + expect(cartManager.createCart).not.toHaveBeenCalled(); + }); }); describe('restartCart', () => { @@ -253,6 +317,7 @@ describe('CartService', () => { offeringConfigId: mockOldCart.offeringConfigId, couponCode: mockOldCart.couponCode, taxAddress: mockOldCart.taxAddress, + currency: mockOldCart.currency, stripeCustomerId: mockOldCart.stripeCustomerId, email: mockOldCart.email, amount: mockOldCart.amount, @@ -485,6 +550,10 @@ describe('CartService', () => { const mockPrice = StripePriceFactory(); const mockUpdateCart = UpdateCartFactory({ couponCode: faker.word.noun(), + taxAddress: { + postalCode: faker.location.zipCode(), + countryCode: faker.location.countryCode(), + }, }); beforeEach(async () => { @@ -492,15 +561,16 @@ describe('CartService', () => { jest .spyOn(productConfigurationManager, 'retrieveStripePrice') .mockResolvedValue(mockPrice); - }); - - it('success if coupon is valid', async () => { + jest + .spyOn(currencyManager, 'getCurrencyForCountry') + .mockReturnValue(faker.finance.currencyCode()); jest .spyOn(promotionCodeManager, 'assertValidPromotionCodeNameForPrice') .mockResolvedValue(undefined); - jest.spyOn(cartManager, 'updateFreshCart').mockResolvedValue(); + }); + it('success if coupon is valid', async () => { await cartService.updateCart( mockCart.id, mockCart.version, @@ -520,7 +590,6 @@ describe('CartService', () => { .mockImplementation(() => { throw new CouponErrorExpired(); }); - jest.spyOn(cartManager, 'updateFreshCart').mockRejectedValue(undefined); await expect( cartService.updateCart(mockCart.id, mockCart.version, mockUpdateCart) @@ -528,6 +597,18 @@ describe('CartService', () => { expect(cartManager.updateFreshCart).not.toHaveBeenCalledWith(); }); + + it('throws if country to currency result is not valid', async () => { + jest + .spyOn(currencyManager, 'getCurrencyForCountry') + .mockReturnValue(undefined); + + await expect( + cartService.updateCart(mockCart.id, mockCart.version, mockUpdateCart) + ).rejects.toBeInstanceOf(CartInvalidCurrencyError); + + expect(cartManager.updateFreshCart).not.toHaveBeenCalledWith(); + }); }); }); diff --git a/libs/payments/cart/src/lib/cart.service.ts b/libs/payments/cart/src/lib/cart.service.ts index 99f441e218..c996b40583 100644 --- a/libs/payments/cart/src/lib/cart.service.ts +++ b/libs/payments/cart/src/lib/cart.service.ts @@ -17,6 +17,7 @@ import { StripeCustomer, } from '@fxa/payments/stripe'; import { ProductConfigurationManager } from '@fxa/shared/cms'; +import { CurrencyManager } from '@fxa/payments/currency'; import { CartErrorReasonId, CartState } from '@fxa/shared/db/mysql/account'; import { GeoDBManager } from '@fxa/shared/geodb'; @@ -29,7 +30,10 @@ import { } from './cart.types'; import { handleEligibilityStatusMap } from './cart.utils'; import { CheckoutService } from './checkout.service'; -import { CartInvalidPromoCodeError } from './cart.error'; +import { + CartInvalidCurrencyError, + CartInvalidPromoCodeError, +} from './cart.error'; @Injectable() export class CartService { @@ -37,6 +41,7 @@ export class CartService { private accountCustomerManager: AccountCustomerManager, private cartManager: CartManager, private checkoutService: CheckoutService, + private currencyManager: CurrencyManager, private customerManager: CustomerManager, private promotionCodeManager: PromotionCodeManager, private eligibilityService: EligibilityService, @@ -113,6 +118,16 @@ export class CartService { } } + let currency: string | undefined; + if (taxAddress?.countryCode) { + currency = this.currencyManager.getCurrencyForCountry( + taxAddress?.countryCode + ); + if (!currency) { + throw new CartInvalidCurrencyError(currency, taxAddress.countryCode); + } + } + const cart = await this.cartManager.createCart({ interval: args.interval, offeringConfigId: args.offeringConfigId, @@ -121,6 +136,7 @@ export class CartService { stripeCustomerId: accountCustomer?.stripeCustomerId || undefined, experiment: args.experiment, taxAddress, + currency, eligibilityStatus: cartEligibilityStatus, }); @@ -156,6 +172,7 @@ export class CartService { offeringConfigId: oldCart.offeringConfigId, experiment: oldCart.experiment || undefined, taxAddress: oldCart.taxAddress || undefined, + currency: oldCart.currency || undefined, couponCode: oldCart.couponCode || undefined, stripeCustomerId: oldCart.stripeCustomerId || undefined, email: oldCart.email || undefined, @@ -249,6 +266,19 @@ export class CartService { price ); } + + if (cartDetails.taxAddress?.countryCode) { + cartDetails.currency = this.currencyManager.getCurrencyForCountry( + cartDetails.taxAddress?.countryCode + ); + if (!cartDetails.currency) { + throw new CartInvalidCurrencyError( + cartDetails.currency, + cartDetails.taxAddress.countryCode + ); + } + } + await this.cartManager.updateFreshCart(cartId, version, cartDetails); } diff --git a/libs/payments/cart/src/lib/cart.types.ts b/libs/payments/cart/src/lib/cart.types.ts index 79c36b82b6..5cddf0e3ab 100644 --- a/libs/payments/cart/src/lib/cart.types.ts +++ b/libs/payments/cart/src/lib/cart.types.ts @@ -52,6 +52,7 @@ export type SetupCart = { offeringConfigId: string; experiment?: string; taxAddress?: TaxAddress; + currency?: string; couponCode?: string; stripeCustomerId?: string; email?: string; @@ -68,6 +69,7 @@ export interface TaxAmount { export type UpdateCart = { uid?: string; taxAddress?: TaxAddress; + currency?: string; couponCode?: string; email?: string; stripeCustomerId?: string; diff --git a/libs/payments/cart/src/lib/checkout.service.spec.ts b/libs/payments/cart/src/lib/checkout.service.spec.ts index d93cb1c9e8..913a6f5f72 100644 --- a/libs/payments/cart/src/lib/checkout.service.spec.ts +++ b/libs/payments/cart/src/lib/checkout.service.spec.ts @@ -68,6 +68,7 @@ import { CartTotalMismatchError, CartEmailNotFoundError, CartInvalidPromoCodeError, + CartInvalidCurrencyError, } from './cart.error'; import { CheckoutService } from './checkout.service'; @@ -347,6 +348,18 @@ describe('CheckoutService', () => { ).rejects.toBeInstanceOf(CartEmailNotFoundError); }); + it('throws cart currency invalid error', async () => { + const mockCart = StripeResponseFactory( + ResultCartFactory({ + currency: null, + }) + ); + + await expect( + checkoutService.prePaySteps(mockCart, mockCustomerData) + ).rejects.toBeInstanceOf(CartInvalidCurrencyError); + }); + it('throws cart eligibility mismatch error', async () => { const mockCart = StripeResponseFactory( ResultCartFactory({ diff --git a/libs/payments/cart/src/lib/checkout.service.ts b/libs/payments/cart/src/lib/checkout.service.ts index 9602823613..b0f061a2ad 100644 --- a/libs/payments/cart/src/lib/checkout.service.ts +++ b/libs/payments/cart/src/lib/checkout.service.ts @@ -34,6 +34,7 @@ import { CartEligibilityMismatchError, CartEmailNotFoundError, CartInvalidPromoCodeError, + CartInvalidCurrencyError, } from './cart.error'; import { CartManager } from './cart.manager'; import { CheckoutCustomerData, ResultCart } from './cart.types'; @@ -65,6 +66,13 @@ export class CheckoutService { throw new CartEmailNotFoundError(cart.id); } + if (!cart.currency) { + throw new CartInvalidCurrencyError( + cart.currency || undefined, + taxAddress.countryCode + ); + } + // if uid not found, create stub account customer // TODO: update hardcoded verifierVersion // https://mozilla-hub.atlassian.net/browse/FXA-9693 diff --git a/libs/payments/currency/src/lib/currency.config.ts b/libs/payments/currency/src/lib/currency.config.ts index 62114ed806..d6e06b62d8 100644 --- a/libs/payments/currency/src/lib/currency.config.ts +++ b/libs/payments/currency/src/lib/currency.config.ts @@ -13,10 +13,18 @@ export class CurrencyConfig { ) @IsObject() public readonly taxIds!: { [key: string]: string }; + + @Transform( + ({ value }) => (value instanceof Object ? value : JSON.parse(value)), + { toClassOnly: true } + ) + @IsObject() + public readonly currenciesToCountries!: { [key: string]: string[] }; } export const MockCurrencyConfig = { taxIds: { EUR: 'EU1234' }, + currenciesToCountries: { USD: ['US'] }, } satisfies CurrencyConfig; export const MockCurrencyConfigProvider = { diff --git a/libs/payments/currency/src/lib/currency.manager.ts b/libs/payments/currency/src/lib/currency.manager.ts index 0648f0505b..91bce67ae9 100644 --- a/libs/payments/currency/src/lib/currency.manager.ts +++ b/libs/payments/currency/src/lib/currency.manager.ts @@ -60,4 +60,16 @@ export class CurrencyManager { getTaxId(currency: string) { return this.taxIds[currency.toUpperCase()]; } + + getCurrencyForCountry(country: string) { + for (const [currency, countries] of Object.entries( + this.config.currenciesToCountries + )) { + if (countries.includes(country)) { + return currency; + } + } + + return undefined; + } } diff --git a/libs/payments/metrics/src/lib/glean/glean.types.ts b/libs/payments/metrics/src/lib/glean/glean.types.ts index 72e2aa54da..89148fa827 100644 --- a/libs/payments/metrics/src/lib/glean/glean.types.ts +++ b/libs/payments/metrics/src/lib/glean/glean.types.ts @@ -14,11 +14,8 @@ export type CommonMetrics = { }; export type CartMetrics = Partial< - Pick -> & { - //TODO - Replace on completion of FXA-7584 and pick from ResultCart - currency: string; -}; + Pick +>; export type FxaPaySetupMetrics = CommonMetrics & CartMetrics; diff --git a/libs/payments/ui/src/lib/nestapp/app.module.ts b/libs/payments/ui/src/lib/nestapp/app.module.ts index 5e99e0af48..fb4ad5c91b 100644 --- a/libs/payments/ui/src/lib/nestapp/app.module.ts +++ b/libs/payments/ui/src/lib/nestapp/app.module.ts @@ -42,6 +42,7 @@ import { import { RootConfig } from './config'; import { NextJSActionsService } from './nextjs-actions.service'; import { validate } from '../config.utils'; +import { CurrencyManager } from '@fxa/payments/currency'; @Module({ imports: [ @@ -71,6 +72,7 @@ import { validate } from '../config.utils'; CartService, CheckoutTokenManager, CustomerManager, + CurrencyManager, CheckoutService, EligibilityManager, EligibilityService, diff --git a/libs/payments/ui/src/lib/nestapp/config.ts b/libs/payments/ui/src/lib/nestapp/config.ts index 63cb2d0666..980c5c89e7 100644 --- a/libs/payments/ui/src/lib/nestapp/config.ts +++ b/libs/payments/ui/src/lib/nestapp/config.ts @@ -13,6 +13,7 @@ import { StrapiClientConfig } from '@fxa/shared/cms'; import { FirestoreConfig } from 'libs/shared/db/firestore/src/lib/firestore.config'; import { StatsDConfig } from 'libs/shared/metrics/statsd/src/lib/statsd.config'; import { PaymentsGleanConfig } from '@fxa/payments/metrics'; +import { CurrencyConfig } from 'libs/payments/currency/src/lib/currency.config'; export class RootConfig { @Type(() => MySQLConfig) @@ -39,6 +40,11 @@ export class RootConfig { @IsDefined() public readonly paypalClientConfig!: Partial; + @Type(() => CurrencyConfig) + @ValidateNested() + @IsDefined() + public readonly currencyConfig!: Partial; + @Type(() => StrapiClientConfig) @ValidateNested() @IsDefined() diff --git a/libs/shared/db/mysql/account/src/lib/factories.ts b/libs/shared/db/mysql/account/src/lib/factories.ts index 481b693d3c..2900e8c385 100644 --- a/libs/shared/db/mysql/account/src/lib/factories.ts +++ b/libs/shared/db/mysql/account/src/lib/factories.ts @@ -30,6 +30,7 @@ export const CartFactory = (override?: Partial): NewCart => ({ 'semiannually', 'annually', ]), + currency: faker.finance.currencyCode(), createdAt: faker.date.recent().getTime(), updatedAt: faker.date.recent().getTime(), amount: faker.number.int(10000), diff --git a/libs/shared/db/mysql/account/src/lib/kysely-types.ts b/libs/shared/db/mysql/account/src/lib/kysely-types.ts index a5975dddbb..da49fc3e77 100644 --- a/libs/shared/db/mysql/account/src/lib/kysely-types.ts +++ b/libs/shared/db/mysql/account/src/lib/kysely-types.ts @@ -95,6 +95,7 @@ export interface Carts { countryCode: string; postalCode: string; }> | null; + currency: string | null; createdAt: number; updatedAt: number; couponCode: string | null; diff --git a/libs/shared/db/mysql/account/src/test/carts.sql b/libs/shared/db/mysql/account/src/test/carts.sql index 62a0b5de3c..063ca2cb5e 100644 --- a/libs/shared/db/mysql/account/src/test/carts.sql +++ b/libs/shared/db/mysql/account/src/test/carts.sql @@ -7,6 +7,7 @@ CREATE TABLE `carts` ( `interval` varchar(255) COLLATE utf8mb4_bin NOT NULL, `experiment` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `taxAddress` json DEFAULT NULL, + `currency` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `createdAt` bigint unsigned NOT NULL, `updatedAt` bigint unsigned NOT NULL, `couponCode` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, diff --git a/packages/db-migrations/databases/fxa/patches/patch-154-155.sql b/packages/db-migrations/databases/fxa/patches/patch-154-155.sql new file mode 100644 index 0000000000..386490ac2e --- /dev/null +++ b/packages/db-migrations/databases/fxa/patches/patch-154-155.sql @@ -0,0 +1,6 @@ +-- Add `currency` column to the `carts` table. +ALTER TABLE carts +ADD COLUMN currency VARCHAR(255) AFTER taxAddress, +ALGORITHM = INPLACE, LOCK = NONE; + +UPDATE dbMetadata SET value = '155' WHERE name = 'schema-patch-level'; diff --git a/packages/db-migrations/databases/fxa/patches/patch-155-154.sql b/packages/db-migrations/databases/fxa/patches/patch-155-154.sql new file mode 100644 index 0000000000..1f1b5066d2 --- /dev/null +++ b/packages/db-migrations/databases/fxa/patches/patch-155-154.sql @@ -0,0 +1,5 @@ +-- Drop `currency` column from the `carts` table. +-- ALTER TABLE carts DROP COLUMN currency, +-- ALGORITHM = INPLACE, LOCK = NONE; +-- +-- UPDATE dbMetadata SET value = '154' WHERE name = 'schema-patch-level'; diff --git a/packages/db-migrations/databases/fxa/target-patch.json b/packages/db-migrations/databases/fxa/target-patch.json index 01aeddbf41..472e6c1741 100644 --- a/packages/db-migrations/databases/fxa/target-patch.json +++ b/packages/db-migrations/databases/fxa/target-patch.json @@ -1,3 +1,3 @@ { - "level": 153 + "level": 155 }