Skip to content

Commit

Permalink
feat(core): Implement per-customer usage limits for Promotions
Browse files Browse the repository at this point in the history
Relates to #174

BREAKING CHANGE: A new `promotions` relation has been added to the order table, and a `perCustomerUsageLimit` column to the promotion table. This will require a DB migration.
  • Loading branch information
michaelbromley committed Oct 9, 2019
1 parent fc796e1 commit 9d45069
Show file tree
Hide file tree
Showing 21 changed files with 422 additions and 100 deletions.
5 changes: 5 additions & 0 deletions packages/common/src/generated-shop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1472,18 +1472,22 @@ export type Order = Node & {
id: Scalars['ID'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
/** A unique code for the Order */
code: Scalars['String'];
state: Scalars['String'];
/** An order is active as long as the payment process has not been completed */
active: Scalars['Boolean'];
customer?: Maybe<Customer>;
shippingAddress?: Maybe<OrderAddress>;
billingAddress?: Maybe<OrderAddress>;
lines: Array<OrderLine>;
/** Order-level adjustments to the order total, such as discounts from promotions */
adjustments: Array<Adjustment>;
couponCodes: Array<Scalars['String']>;
payments?: Maybe<Array<Payment>>;
fulfillments?: Maybe<Array<Fulfillment>>;
subTotalBeforeTax: Scalars['Int'];
/** The subTotal is the total of the OrderLines, before order-level promotions and shipping has been applied. */
subTotal: Scalars['Int'];
currencyCode: CurrencyCode;
shipping: Scalars['Int'];
Expand Down Expand Up @@ -1858,6 +1862,7 @@ export type Promotion = Node & {
startsAt?: Maybe<Scalars['DateTime']>;
endsAt?: Maybe<Scalars['DateTime']>;
couponCode?: Maybe<Scalars['String']>;
perCustomerUsageLimit?: Maybe<Scalars['Int']>;
name: Scalars['String'];
enabled: Scalars['Boolean'];
conditions: Array<ConfigurableOperation>;
Expand Down
9 changes: 9 additions & 0 deletions packages/common/src/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ export type CreatePromotionInput = {
startsAt?: Maybe<Scalars['DateTime']>,
endsAt?: Maybe<Scalars['DateTime']>,
couponCode?: Maybe<Scalars['String']>,
perCustomerUsageLimit?: Maybe<Scalars['Int']>,
conditions: Array<ConfigurableOperationInput>,
actions: Array<ConfigurableOperationInput>,
};
Expand Down Expand Up @@ -2160,18 +2161,22 @@ export type Order = Node & {
id: Scalars['ID'],
createdAt: Scalars['DateTime'],
updatedAt: Scalars['DateTime'],
/** A unique code for the Order */
code: Scalars['String'],
state: Scalars['String'],
/** An order is active as long as the payment process has not been completed */
active: Scalars['Boolean'],
customer?: Maybe<Customer>,
shippingAddress?: Maybe<OrderAddress>,
billingAddress?: Maybe<OrderAddress>,
lines: Array<OrderLine>,
/** Order-level adjustments to the order total, such as discounts from promotions */
adjustments: Array<Adjustment>,
couponCodes: Array<Scalars['String']>,
payments?: Maybe<Array<Payment>>,
fulfillments?: Maybe<Array<Fulfillment>>,
subTotalBeforeTax: Scalars['Int'],
/** The subTotal is the total of the OrderLines, before order-level promotions and shipping has been applied. */
subTotal: Scalars['Int'],
currencyCode: CurrencyCode,
shipping: Scalars['Int'],
Expand Down Expand Up @@ -2613,6 +2618,7 @@ export type Promotion = Node & {
startsAt?: Maybe<Scalars['DateTime']>,
endsAt?: Maybe<Scalars['DateTime']>,
couponCode?: Maybe<Scalars['String']>,
perCustomerUsageLimit?: Maybe<Scalars['Int']>,
name: Scalars['String'],
enabled: Scalars['Boolean'],
conditions: Array<ConfigurableOperation>,
Expand All @@ -2625,6 +2631,7 @@ export type PromotionFilterParameter = {
startsAt?: Maybe<DateOperators>,
endsAt?: Maybe<DateOperators>,
couponCode?: Maybe<StringOperators>,
perCustomerUsageLimit?: Maybe<NumberOperators>,
name?: Maybe<StringOperators>,
enabled?: Maybe<BooleanOperators>,
};
Expand All @@ -2649,6 +2656,7 @@ export type PromotionSortParameter = {
startsAt?: Maybe<SortOrder>,
endsAt?: Maybe<SortOrder>,
couponCode?: Maybe<SortOrder>,
perCustomerUsageLimit?: Maybe<SortOrder>,
name?: Maybe<SortOrder>,
};

Expand Down Expand Up @@ -3388,6 +3396,7 @@ export type UpdatePromotionInput = {
startsAt?: Maybe<Scalars['DateTime']>,
endsAt?: Maybe<Scalars['DateTime']>,
couponCode?: Maybe<Scalars['String']>,
perCustomerUsageLimit?: Maybe<Scalars['Int']>,
conditions?: Maybe<Array<ConfigurableOperationInput>>,
actions?: Maybe<Array<ConfigurableOperationInput>>,
};
Expand Down
9 changes: 9 additions & 0 deletions packages/core/e2e/graphql/generated-e2e-admin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ export type CreatePromotionInput = {
startsAt?: Maybe<Scalars['DateTime']>;
endsAt?: Maybe<Scalars['DateTime']>;
couponCode?: Maybe<Scalars['String']>;
perCustomerUsageLimit?: Maybe<Scalars['Int']>;
conditions: Array<ConfigurableOperationInput>;
actions: Array<ConfigurableOperationInput>;
};
Expand Down Expand Up @@ -2095,18 +2096,22 @@ export type Order = Node & {
id: Scalars['ID'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
/** A unique code for the Order */
code: Scalars['String'];
state: Scalars['String'];
/** An order is active as long as the payment process has not been completed */
active: Scalars['Boolean'];
customer?: Maybe<Customer>;
shippingAddress?: Maybe<OrderAddress>;
billingAddress?: Maybe<OrderAddress>;
lines: Array<OrderLine>;
/** Order-level adjustments to the order total, such as discounts from promotions */
adjustments: Array<Adjustment>;
couponCodes: Array<Scalars['String']>;
payments?: Maybe<Array<Payment>>;
fulfillments?: Maybe<Array<Fulfillment>>;
subTotalBeforeTax: Scalars['Int'];
/** The subTotal is the total of the OrderLines, before order-level promotions and shipping has been applied. */
subTotal: Scalars['Int'];
currencyCode: CurrencyCode;
shipping: Scalars['Int'];
Expand Down Expand Up @@ -2546,6 +2551,7 @@ export type Promotion = Node & {
startsAt?: Maybe<Scalars['DateTime']>;
endsAt?: Maybe<Scalars['DateTime']>;
couponCode?: Maybe<Scalars['String']>;
perCustomerUsageLimit?: Maybe<Scalars['Int']>;
name: Scalars['String'];
enabled: Scalars['Boolean'];
conditions: Array<ConfigurableOperation>;
Expand All @@ -2558,6 +2564,7 @@ export type PromotionFilterParameter = {
startsAt?: Maybe<DateOperators>;
endsAt?: Maybe<DateOperators>;
couponCode?: Maybe<StringOperators>;
perCustomerUsageLimit?: Maybe<NumberOperators>;
name?: Maybe<StringOperators>;
enabled?: Maybe<BooleanOperators>;
};
Expand All @@ -2582,6 +2589,7 @@ export type PromotionSortParameter = {
startsAt?: Maybe<SortOrder>;
endsAt?: Maybe<SortOrder>;
couponCode?: Maybe<SortOrder>;
perCustomerUsageLimit?: Maybe<SortOrder>;
name?: Maybe<SortOrder>;
};

Expand Down Expand Up @@ -3287,6 +3295,7 @@ export type UpdatePromotionInput = {
startsAt?: Maybe<Scalars['DateTime']>;
endsAt?: Maybe<Scalars['DateTime']>;
couponCode?: Maybe<Scalars['String']>;
perCustomerUsageLimit?: Maybe<Scalars['Int']>;
conditions?: Maybe<Array<ConfigurableOperationInput>>;
actions?: Maybe<Array<ConfigurableOperationInput>>;
};
Expand Down
7 changes: 6 additions & 1 deletion packages/core/e2e/graphql/generated-e2e-shop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1472,18 +1472,22 @@ export type Order = Node & {
id: Scalars['ID'];
createdAt: Scalars['DateTime'];
updatedAt: Scalars['DateTime'];
/** A unique code for the Order */
code: Scalars['String'];
state: Scalars['String'];
/** An order is active as long as the payment process has not been completed */
active: Scalars['Boolean'];
customer?: Maybe<Customer>;
shippingAddress?: Maybe<OrderAddress>;
billingAddress?: Maybe<OrderAddress>;
lines: Array<OrderLine>;
/** Order-level adjustments to the order total, such as discounts from promotions */
adjustments: Array<Adjustment>;
couponCodes: Array<Scalars['String']>;
payments?: Maybe<Array<Payment>>;
fulfillments?: Maybe<Array<Fulfillment>>;
subTotalBeforeTax: Scalars['Int'];
/** The subTotal is the total of the OrderLines, before order-level promotions and shipping has been applied. */
subTotal: Scalars['Int'];
currencyCode: CurrencyCode;
shipping: Scalars['Int'];
Expand Down Expand Up @@ -1858,6 +1862,7 @@ export type Promotion = Node & {
startsAt?: Maybe<Scalars['DateTime']>;
endsAt?: Maybe<Scalars['DateTime']>;
couponCode?: Maybe<Scalars['String']>;
perCustomerUsageLimit?: Maybe<Scalars['Int']>;
name: Scalars['String'];
enabled: Scalars['Boolean'];
conditions: Array<ConfigurableOperation>;
Expand Down Expand Up @@ -2210,7 +2215,7 @@ export type Zone = Node & {
};
export type TestOrderFragmentFragment = { __typename?: 'Order' } & Pick<
Order,
'id' | 'code' | 'state' | 'active' | 'total' | 'shipping'
'id' | 'code' | 'state' | 'active' | 'total' | 'couponCodes' | 'shipping'
> & {
adjustments: Array<
{ __typename?: 'Adjustment' } & Pick<
Expand Down
1 change: 1 addition & 0 deletions packages/core/e2e/graphql/shop-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const TEST_ORDER_FRAGMENT = gql`
state
active
total
couponCodes
adjustments {
adjustmentSource
amount
Expand Down
67 changes: 3 additions & 64 deletions packages/core/e2e/order.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import path from 'path';

import { HistoryEntryType, StockMovementType } from '../../common/lib/generated-types';
import { pick } from '../../common/lib/pick';
import { ID } from '../../common/lib/shared-types';
import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';

import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
Expand All @@ -29,32 +28,18 @@ import {
SettleRefund,
UpdateProductVariants,
} from './graphql/generated-e2e-admin-types';
import {
AddItemToOrder,
AddPaymentToOrder,
AddPaymentToOrderMutation,
GetShippingMethods,
SetShippingAddress,
SetShippingMethod,
TransitionToState,
} from './graphql/generated-e2e-shop-types';
import { AddItemToOrder } from './graphql/generated-e2e-shop-types';
import {
GET_CUSTOMER_LIST,
GET_PRODUCT_WITH_VARIANTS,
GET_STOCK_MOVEMENT,
UPDATE_PRODUCT_VARIANTS,
} from './graphql/shared-definitions';
import {
ADD_ITEM_TO_ORDER,
ADD_PAYMENT,
GET_ELIGIBLE_SHIPPING_METHODS,
SET_SHIPPING_ADDRESS,
SET_SHIPPING_METHOD,
TRANSITION_TO_STATE,
} from './graphql/shop-definitions';
import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
import { TestAdminClient, TestShopClient } from './test-client';
import { TestServer } from './test-server';
import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';

describe('Orders resolver', () => {
const adminClient = new TestAdminClient();
Expand Down Expand Up @@ -1216,33 +1201,6 @@ const failsToSettlePaymentMethod = new PaymentMethodHandler({
},
});

async function proceedToArrangingPayment(shopClient: TestShopClient): Promise<ID> {
await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(SET_SHIPPING_ADDRESS, {
input: {
fullName: 'name',
streetLine1: '12 the street',
city: 'foo',
postalCode: '123456',
countryCode: 'US',
},
});

const { eligibleShippingMethods } = await shopClient.query<GetShippingMethods.Query>(
GET_ELIGIBLE_SHIPPING_METHODS,
);

await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(SET_SHIPPING_METHOD, {
id: eligibleShippingMethods[1].id,
});

const { transitionOrderToState } = await shopClient.query<
TransitionToState.Mutation,
TransitionToState.Variables
>(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });

return transitionOrderToState!.id;
}

async function createTestOrder(
adminClient: TestAdminClient,
shopClient: TestShopClient,
Expand Down Expand Up @@ -1288,25 +1246,6 @@ async function createTestOrder(
return { product, productVariantId, orderId };
}

async function addPaymentToOrder(
shopClient: TestShopClient,
handler: PaymentMethodHandler,
): Promise<NonNullable<AddPaymentToOrder.Mutation['addPaymentToOrder']>> {
const result = await shopClient.query<AddPaymentToOrder.Mutation, AddPaymentToOrder.Variables>(
ADD_PAYMENT,
{
input: {
method: handler.code,
metadata: {
baz: 'quux',
},
},
},
);
const order = result.addPaymentToOrder!;
return order as any;
}

export const GET_ORDERS_LIST = gql`
query GetOrderList($options: OrderListOptions) {
orders(options: $options) {
Expand Down
30 changes: 7 additions & 23 deletions packages/core/e2e/shop-order.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
import { TestAdminClient, TestShopClient } from './test-client';
import { TestServer } from './test-server';
import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
import { testSuccessfulPaymentMethod } from './utils/test-order-utils';

describe('Shop orders', () => {
const adminClient = new TestAdminClient();
Expand All @@ -70,7 +71,7 @@ describe('Shop orders', () => {
{
paymentOptions: {
paymentMethodHandlers: [
testPaymentMethod,
testSuccessfulPaymentMethod,
testFailingPaymentMethod,
testErrorPaymentMethod,
],
Expand Down Expand Up @@ -399,7 +400,7 @@ describe('Shop orders', () => {
AddPaymentToOrder.Variables
>(ADD_PAYMENT, {
input: {
method: testPaymentMethod.code,
method: testSuccessfulPaymentMethod.code,
metadata: {},
},
});
Expand All @@ -408,7 +409,7 @@ describe('Shop orders', () => {
expect(addPaymentToOrder!.state).toBe('PaymentSettled');
expect(addPaymentToOrder!.active).toBe(false);
expect(addPaymentToOrder!.payments!.length).toBe(1);
expect(payment.method).toBe(testPaymentMethod.code);
expect(payment.method).toBe(testSuccessfulPaymentMethod.code);
expect(payment.state).toBe('Settled');
});

Expand Down Expand Up @@ -660,7 +661,7 @@ describe('Shop orders', () => {
ADD_PAYMENT,
{
input: {
method: testPaymentMethod.code,
method: testSuccessfulPaymentMethod.code,
metadata: {},
},
},
Expand Down Expand Up @@ -796,7 +797,7 @@ describe('Shop orders', () => {
AddPaymentToOrder.Variables
>(ADD_PAYMENT, {
input: {
method: testPaymentMethod.code,
method: testSuccessfulPaymentMethod.code,
metadata: {
baz: 'quux',
},
Expand All @@ -807,7 +808,7 @@ describe('Shop orders', () => {
expect(addPaymentToOrder!.state).toBe('PaymentSettled');
expect(addPaymentToOrder!.active).toBe(false);
expect(addPaymentToOrder!.payments!.length).toBe(3);
expect(payment.method).toBe(testPaymentMethod.code);
expect(payment.method).toBe(testSuccessfulPaymentMethod.code);
expect(payment.state).toBe('Settled');
expect(payment.transactionId).toBe('12345');
expect(payment.metadata).toEqual({
Expand Down Expand Up @@ -867,23 +868,6 @@ describe('Shop orders', () => {
});
});

const testPaymentMethod = new PaymentMethodHandler({
code: 'test-payment-method',
description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
args: {},
createPayment: (order, args, metadata) => {
return {
amount: order.total,
state: 'Settled',
transactionId: '12345',
metadata,
};
},
settlePayment: order => ({
success: true,
}),
});

const testFailingPaymentMethod = new PaymentMethodHandler({
code: 'test-failing-payment-method',
description: [{ languageCode: LanguageCode.en, value: 'Test Failing Payment Method' }],
Expand Down
Loading

0 comments on commit 9d45069

Please sign in to comment.