зеркало из https://github.com/mozilla/fxa.git
Merge pull request #17668 from mozilla/FXA-7584
feat(payments-cart): add currency to cart
This commit is contained in:
Коммит
025f8caee2
|
@ -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=
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -54,7 +54,6 @@ export default async function Checkout({
|
|||
getApp()
|
||||
.getGleanEmitter()
|
||||
.emit('fxaPaySetupView', {
|
||||
currency: 'USD',
|
||||
checkoutType: 'without-accounts',
|
||||
params: { ...params },
|
||||
searchParams,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ export type SetupCart = {
|
|||
offeringConfigId: string;
|
||||
experiment?: string;
|
||||
taxAddress?: TaxAddress;
|
||||
currency?: string;
|
||||
couponCode?: string;
|
||||
stripeCustomerId?: string;
|
||||
email?: string;
|
||||
|
@ -70,6 +71,7 @@ export interface TaxAmount {
|
|||
export type UpdateCart = {
|
||||
uid?: string;
|
||||
taxAddress?: TaxAddress;
|
||||
currency?: string;
|
||||
couponCode?: string;
|
||||
email?: string;
|
||||
stripeCustomerId?: string;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,11 +14,8 @@ export type CommonMetrics = {
|
|||
};
|
||||
|
||||
export type CartMetrics = Partial<
|
||||
Pick<ResultCart, 'uid' | 'errorReasonId' | 'couponCode'>
|
||||
> & {
|
||||
//TODO - Replace on completion of FXA-7584 and pick from ResultCart
|
||||
currency: string;
|
||||
};
|
||||
Pick<ResultCart, 'uid' | 'errorReasonId' | 'couponCode' | 'currency'>
|
||||
>;
|
||||
|
||||
export type FxaPaySetupMetrics = CommonMetrics & CartMetrics;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<PaypalClientConfig>;
|
||||
|
||||
@Type(() => CurrencyConfig)
|
||||
@ValidateNested()
|
||||
@IsDefined()
|
||||
public readonly currencyConfig!: Partial<CurrencyConfig>;
|
||||
|
||||
@Type(() => StrapiClientConfig)
|
||||
@ValidateNested()
|
||||
@IsDefined()
|
||||
|
|
|
@ -30,6 +30,7 @@ export const CartFactory = (override?: Partial<NewCart>): NewCart => ({
|
|||
'semiannually',
|
||||
'annually',
|
||||
]),
|
||||
currency: faker.finance.currencyCode(),
|
||||
createdAt: faker.date.recent().getTime(),
|
||||
updatedAt: faker.date.recent().getTime(),
|
||||
amount: faker.number.int(10000),
|
||||
|
|
|
@ -95,6 +95,7 @@ export interface Carts {
|
|||
countryCode: string;
|
||||
postalCode: string;
|
||||
}> | null;
|
||||
currency: string | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
couponCode: string | null;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"level": 153
|
||||
"level": 155
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче