diff --git a/packages/core/e2e/custom-fields.e2e-spec.ts b/packages/core/e2e/custom-fields.e2e-spec.ts index ef42aae622..b5db0bd396 100644 --- a/packages/core/e2e/custom-fields.e2e-spec.ts +++ b/packages/core/e2e/custom-fields.e2e-spec.ts @@ -192,6 +192,16 @@ const customConfig = mergeConfig(testConfig(), { { name: 'costPrice', type: 'int', + } + ], + // Single readonly Address custom field to test + // https://github.com/vendure-ecommerce/vendure/issues/3326 + Address: [ + { + name: 'hereId', + type: 'string', + readonly: true, + nullable: true, }, ], } as CustomFields, diff --git a/packages/core/src/api/config/graphql-custom-fields.ts b/packages/core/src/api/config/graphql-custom-fields.ts index 7d16a74978..c8c68c4164 100644 --- a/packages/core/src/api/config/graphql-custom-fields.ts +++ b/packages/core/src/api/config/graphql-custom-fields.ts @@ -247,6 +247,7 @@ export function addGraphQLCustomFields( const publicAddressFields = customFieldConfig.Address?.filter( config => !config.internal && (publicOnly === true ? config.public !== false : true), ); + const writeablePublicAddressFields = publicAddressFields?.filter(field => !field.readonly); if (publicAddressFields?.length) { // For custom fields on the Address entity, we also extend the OrderAddress // type (which is used to store address snapshots on Orders) @@ -257,7 +258,7 @@ export function addGraphQLCustomFields( } `; } - if (schema.getType('UpdateOrderAddressInput')) { + if (schema.getType('UpdateOrderAddressInput') && writeablePublicAddressFields?.length) { customFieldTypeDefs += ` extend input UpdateOrderAddressInput { customFields: UpdateAddressCustomFieldsInput diff --git a/packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts b/packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts index 206a11af42..650fc44c1f 100644 --- a/packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts +++ b/packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts @@ -1182,6 +1182,71 @@ describe('OrderCalculator', () => { expect(order.subTotalWithTax).toBe(5719); }); }); + + it(`clear previous promotion state before testing`, async () => { + const noLineDiscountsCondition = new PromotionCondition({ + args: {}, + code: 'no_other_discounts_condition', + description: [{ languageCode: LanguageCode.en, value: '' }], + check(_ctx, _order) { + const linesToDiscount = _order.lines + .filter(line => !line.adjustments.length) + .map(line => line.id); + return linesToDiscount.length ? { lines: linesToDiscount } : false; + }, + }); + const discountMatchedLinesAction = new PromotionItemAction({ + code: 'discount_matched_lines_action', + conditions: [noLineDiscountsCondition], + description: [{ languageCode: LanguageCode.en, value: '' }], + args: { discount: { type: 'int' } }, + execute(_ctx, orderLine, args, state) { + if (state.no_other_discounts_condition.lines.includes(orderLine.id)) { + return -args.discount; + } + return 0; + }, + }); + + const discountAllUndiscountedItems = new Promotion({ + id: 1, + name: 'Discount all undiscounted items', + conditions: [ + { + code: noLineDiscountsCondition.code, + args: [], + }, + ], + promotionConditions: [noLineDiscountsCondition], + actions: [ + { + code: discountMatchedLinesAction.code, + args: [{ name: 'discount', value: '50' }], + }, + ], + promotionActions: [discountMatchedLinesAction], + }); + + const ctx = createRequestContext({ pricesIncludeTax: false }); + const order = createOrder({ + ctx, + lines: [ + { + listPrice: 500, + taxCategory: taxCategoryStandard, + quantity: 2, + }, + ], + }); + + await orderCalculator.applyPriceAdjustments(ctx, order, [discountAllUndiscountedItems]); + // everything gets discounted by 50 + expect(order.subTotal).toBe(900); + // should still be discounted after changing quantity + order.lines[0].quantity = 3; + await orderCalculator.applyPriceAdjustments(ctx, order, [discountAllUndiscountedItems]); + expect(order.subTotal).toBe(1350); + }); }); }); diff --git a/packages/core/src/service/helpers/order-calculator/order-calculator.ts b/packages/core/src/service/helpers/order-calculator/order-calculator.ts index d9eb069bf7..889e206652 100644 --- a/packages/core/src/service/helpers/order-calculator/order-calculator.ts +++ b/packages/core/src/service/helpers/order-calculator/order-calculator.ts @@ -186,8 +186,8 @@ export class OrderCalculator { for (const line of order.lines) { // Must be re-calculated for each line, since the previous lines may have triggered promotions // which affected the order price. - const applicablePromotions = await filterAsync(promotions, p => p.test(ctx, order).then(Boolean)); line.clearAdjustments(); + const applicablePromotions = await filterAsync(promotions, p => p.test(ctx, order).then(Boolean)); for (const promotion of applicablePromotions) { let priceAdjusted = false; diff --git a/packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts b/packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts index cd30eeed03..c9776c1904 100644 --- a/packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts +++ b/packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts @@ -175,6 +175,7 @@ describe('Stripe payments', () => { 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN, 'metadata[orderId]': '1', 'metadata[orderCode]': activeOrder?.code, + 'metadata[languageCode]': 'en', }); expect(createStripePaymentIntent).toEqual('test-client-secret'); }); @@ -207,6 +208,7 @@ describe('Stripe payments', () => { 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN, 'metadata[orderId]': '1', 'metadata[orderCode]': activeOrder?.code, + 'metadata[languageCode]': 'en', 'metadata[customerEmail]': customers[0].emailAddress, }); expect(createStripePaymentIntent).toEqual('test-client-secret'); @@ -240,6 +242,7 @@ describe('Stripe payments', () => { description: `Order #${activeOrder!.code} for ${activeOrder!.customer!.emailAddress}`, 'automatic_payment_methods[enabled]': 'true', 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN, + 'metadata[languageCode]': 'en', 'metadata[orderId]': '1', 'metadata[orderCode]': activeOrder?.code, }); @@ -280,6 +283,7 @@ describe('Stripe payments', () => { 'automatic_payment_methods[enabled]': 'true', 'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN, 'metadata[orderId]': '1', + 'metadata[languageCode]': 'en', 'metadata[orderCode]': activeOrder?.code, }); expect(connectedAccountHeader).toEqual('acct_connected'); diff --git a/packages/payments-plugin/src/braintree/braintree.handler.ts b/packages/payments-plugin/src/braintree/braintree.handler.ts index 116a2d340c..7834552da8 100644 --- a/packages/payments-plugin/src/braintree/braintree.handler.ts +++ b/packages/payments-plugin/src/braintree/braintree.handler.ts @@ -106,12 +106,12 @@ async function processPayment( }, }); const extractMetadataFn = pluginOptions.extractMetadata ?? defaultExtractMetadataFn; - const metadata = extractMetadataFn(response.transaction); + const metadata = response.transaction && extractMetadataFn(response.transaction); if (!response.success) { return { amount, state: 'Declined' as const, - transactionId: response.transaction.id, + transactionId: response.transaction?.id, errorMessage: response.message, metadata, };