Skip to content

Commit

Permalink
feat(core): Allow order shipping method to be modified
Browse files Browse the repository at this point in the history
Closes #978
  • Loading branch information
michaelbromley committed Jan 26, 2024
1 parent 0750fb1 commit 400d78a
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2499,6 +2499,8 @@ export type ModifyOrderInput = {
*/
refund?: InputMaybe<AdministratorRefundInput>;
refunds?: InputMaybe<Array<AdministratorRefundInput>>;
/** Added in v2.2 */
shippingMethodIds?: InputMaybe<Array<Scalars['ID']['input']>>;
surcharges?: InputMaybe<Array<SurchargeInput>>;
updateBillingAddress?: InputMaybe<UpdateOrderAddressInput>;
updateShippingAddress?: InputMaybe<UpdateOrderAddressInput>;
Expand All @@ -2509,7 +2511,7 @@ export type ModifyOrderOptions = {
recalculateShipping?: InputMaybe<Scalars['Boolean']['input']>;
};

export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | IneligibleShippingMethodError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;

export type MoveCollectionInput = {
collectionId: Scalars['ID']['input'];
Expand Down
4 changes: 3 additions & 1 deletion packages/common/src/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2580,6 +2580,8 @@ export type ModifyOrderInput = {
*/
refund?: InputMaybe<AdministratorRefundInput>;
refunds?: InputMaybe<Array<AdministratorRefundInput>>;
/** Added in v2.2 */
shippingMethodIds?: InputMaybe<Array<Scalars['ID']['input']>>;
surcharges?: InputMaybe<Array<SurchargeInput>>;
updateBillingAddress?: InputMaybe<UpdateOrderAddressInput>;
updateShippingAddress?: InputMaybe<UpdateOrderAddressInput>;
Expand All @@ -2590,7 +2592,7 @@ export type ModifyOrderOptions = {
recalculateShipping?: InputMaybe<Scalars['Boolean']['input']>;
};

export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;
export type ModifyOrderResult = CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError | IneligibleShippingMethodError | InsufficientStockError | NegativeQuantityError | NoChangesSpecifiedError | Order | OrderLimitError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError;

export type MoveCollectionInput = {
collectionId: Scalars['ID']['input'];
Expand Down
20 changes: 11 additions & 9 deletions packages/core/e2e/graphql/generated-e2e-admin-types.ts

Large diffs are not rendered by default.

89 changes: 85 additions & 4 deletions packages/core/e2e/order-modification.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,24 @@ const SHIPPING_OTHER = 750;
const testCalculator = new ShippingCalculator({
code: 'test-calculator',
description: [{ languageCode: LanguageCode.en, value: 'Has metadata' }],
args: {},
args: {
surcharge: {
type: 'int',
defaultValue: 0,
},
},
calculate: (ctx, order, args) => {
let price;
const surcharge = args.surcharge || 0;
switch (order.shippingAddress.countryCode) {
case 'GB':
price = SHIPPING_GB;
price = SHIPPING_GB + surcharge;
break;
case 'US':
price = SHIPPING_US;
price = SHIPPING_US + surcharge;
break;
default:
price = SHIPPING_OTHER;
price = SHIPPING_OTHER + surcharge;
}
return {
price,
Expand Down Expand Up @@ -113,6 +119,7 @@ describe('Order modification', () => {

let orderId: string;
let testShippingMethodId: string;
let testExpressShippingMethodId: string;
const orderGuard: ErrorResultGuard<
UpdatedOrderFragment | OrderWithModificationsFragment | OrderFragment
> = createErrorResultGuard(input => !!input.id);
Expand Down Expand Up @@ -186,6 +193,38 @@ describe('Order modification', () => {
});
testShippingMethodId = createShippingMethod.id;

const { createShippingMethod: shippingMethod2 } = await adminClient.query<
Codegen.CreateShippingMethodMutation,
Codegen.CreateShippingMethodMutationVariables
>(CREATE_SHIPPING_METHOD, {
input: {
code: 'new-method-express',
fulfillmentHandler: manualFulfillmentHandler.code,
checker: {
code: defaultShippingEligibilityChecker.code,
arguments: [
{
name: 'orderMinimum',
value: '0',
},
],
},
calculator: {
code: testCalculator.code,
arguments: [
{
name: 'surcharge',
value: '500',
},
],
},
translations: [
{ languageCode: LanguageCode.en, name: 'test method express', description: '' },
],
},
});
testExpressShippingMethodId = shippingMethod2.id;

// create an order and check out
await shopClient.asUserWithCredentials('[email protected]', 'test');
await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
Expand Down Expand Up @@ -663,6 +702,40 @@ describe('Order modification', () => {
await assertOrderIsUnchanged(order!);
});

it('changing shipping method', async () => {
const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
GET_ORDER,
{
id: orderId,
},
);
const { modifyOrder } = await adminClient.query<
Codegen.ModifyOrderMutation,
Codegen.ModifyOrderMutationVariables
>(MODIFY_ORDER, {
input: {
dryRun: true,
orderId,
shippingMethodIds: [testExpressShippingMethodId],
},
});
orderGuard.assertSuccess(modifyOrder);

const expectedTotal = order!.totalWithTax + 500;
expect(modifyOrder.totalWithTax).toBe(expectedTotal);
expect(modifyOrder.shippingLines).toEqual([
{
id: 'T_1',
discountedPriceWithTax: 1500,
shippingMethod: {
id: testExpressShippingMethodId,
name: 'test method express',
},
},
]);
await assertOrderIsUnchanged(order!);
});

it('does not add a history entry', async () => {
const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
GET_ORDER,
Expand Down Expand Up @@ -2511,6 +2584,14 @@ export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
countryCode
country
}
shippingLines {
id
discountedPriceWithTax
shippingMethod {
id
name
}
}
}
`;

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/api/schema/admin-api/order.api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ input ModifyOrderInput {
refunds: [AdministratorRefundInput!]
options: ModifyOrderOptions
couponCodes: [String!]
"""
Added in v2.2
"""
shippingMethodIds: [ID!]
}

input AddItemInput {
Expand Down Expand Up @@ -452,5 +456,6 @@ union ModifyOrderResult =
| CouponCodeExpiredError
| CouponCodeInvalidError
| CouponCodeLimitError
| IneligibleShippingMethodError
union AddManualPaymentToOrderResult = Order | ManualPaymentStateError
union SetCustomerForDraftOrderResult = Order | EmailAddressConflictError
83 changes: 77 additions & 6 deletions packages/core/src/service/helpers/order-modifier/order-modifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,21 @@ import {
RefundPaymentIdMissingError,
} from '../../../common/error/generated-graphql-admin-errors';
import {
IneligibleShippingMethodError,
InsufficientStockError,
NegativeQuantityError,
OrderLimitError,
} from '../../../common/error/generated-graphql-shop-errors';
import { assertFound, idsAreEqual } from '../../../common/utils';
import { idsAreEqual } from '../../../common/utils';
import { ConfigService } from '../../../config/config.service';
import { CustomFieldConfig } from '../../../config/custom-field/custom-field-types';
import { TransactionalConnection } from '../../../connection/transactional-connection';
import { VendureEntity } from '../../../entity/base/base.entity';
import { Order } from '../../../entity/order/order.entity';
import { OrderLine } from '../../../entity/order-line/order-line.entity';
import { FulfillmentLine } from '../../../entity/order-line-reference/fulfillment-line.entity';
import { OrderModificationLine } from '../../../entity/order-line-reference/order-modification-line.entity';
import { OrderLine } from '../../../entity/order-line/order-line.entity';
import { OrderModification } from '../../../entity/order-modification/order-modification.entity';
import { Order } from '../../../entity/order/order.entity';
import { Payment } from '../../../entity/payment/payment.entity';
import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
import { ShippingLine } from '../../../entity/shipping-line/shipping-line.entity';
Expand All @@ -58,8 +59,8 @@ import { ProductVariantService } from '../../services/product-variant.service';
import { PromotionService } from '../../services/promotion.service';
import { StockMovementService } from '../../services/stock-movement.service';
import { CustomFieldRelationService } from '../custom-field-relation/custom-field-relation.service';
import { EntityHydrator } from '../entity-hydrator/entity-hydrator.service';
import { OrderCalculator } from '../order-calculator/order-calculator';
import { ShippingCalculator } from '../shipping-calculator/shipping-calculator';
import { TranslatorService } from '../translator/translator.service';
import { getOrdersFromLines, orderLinesAreAllCancelled } from '../utils/order-utils';
import { patchEntity } from '../utils/patch-entity';
Expand Down Expand Up @@ -90,7 +91,7 @@ export class OrderModifier {
private customFieldRelationService: CustomFieldRelationService,
private promotionService: PromotionService,
private eventBus: EventBus,
private entityHydrator: EntityHydrator,
private shippingCalculator: ShippingCalculator,
private historyService: HistoryService,
private translator: TranslatorService,
) {}
Expand Down Expand Up @@ -597,6 +598,12 @@ export class OrderModifier {
const updatedOrderLines = order.lines.filter(l => updatedOrderLineIds.includes(l.id));
const promotions = await this.promotionService.getActivePromotionsInChannel(ctx);
const activePromotionsPre = await this.promotionService.getActivePromotionsOnOrder(ctx, order.id);
if (input.shippingMethodIds) {
const result = await this.setShippingMethods(ctx, order, input.shippingMethodIds);
if (isGraphQlErrorResult(result)) {
return result;
}
}
await this.orderCalculator.applyPriceAdjustments(ctx, order, promotions, updatedOrderLines, {
recalculateShipping: input.options?.recalculateShipping,
});
Expand Down Expand Up @@ -657,6 +664,69 @@ export class OrderModifier {
return { order, modification: createdModification };
}

async setShippingMethods(ctx: RequestContext, order: Order, shippingMethodIds: ID[]) {
for (const [i, shippingMethodId] of shippingMethodIds.entries()) {
const shippingMethod = await this.shippingCalculator.getMethodIfEligible(
ctx,
order,
shippingMethodId,
);
if (!shippingMethod) {
return new IneligibleShippingMethodError();
}
let shippingLine: ShippingLine | undefined = order.shippingLines[i];
if (shippingLine) {
shippingLine.shippingMethod = shippingMethod;
shippingLine.shippingMethodId = shippingMethod.id;
} else {
shippingLine = await this.connection.getRepository(ctx, ShippingLine).save(
new ShippingLine({
shippingMethod,
order,
adjustments: [],
listPrice: 0,
listPriceIncludesTax: ctx.channel.pricesIncludeTax,
taxLines: [],
}),
);
if (order.shippingLines) {
order.shippingLines.push(shippingLine);
} else {
order.shippingLines = [shippingLine];
}
}

await this.connection.getRepository(ctx, ShippingLine).save(shippingLine);
}
// remove any now-unused ShippingLines
if (shippingMethodIds.length < order.shippingLines.length) {
const shippingLinesToDelete = order.shippingLines.splice(shippingMethodIds.length - 1);
await this.connection.getRepository(ctx, ShippingLine).remove(shippingLinesToDelete);
}
// assign the ShippingLines to the OrderLines
await this.connection
.getRepository(ctx, OrderLine)
.createQueryBuilder('line')
.update({ shippingLine: undefined })
.whereInIds(order.lines.map(l => l.id))
.execute();
const { shippingLineAssignmentStrategy } = this.configService.shippingOptions;
for (const shippingLine of order.shippingLines) {
const orderLinesForShippingLine =
await shippingLineAssignmentStrategy.assignShippingLineToOrderLines(ctx, shippingLine, order);
await this.connection
.getRepository(ctx, OrderLine)
.createQueryBuilder('line')
.update({ shippingLineId: shippingLine.id })
.whereInIds(orderLinesForShippingLine.map(l => l.id))
.execute();
orderLinesForShippingLine.forEach(line => {
line.shippingLine = shippingLine;
});
}
return order;
}

private noChangesSpecified(input: ModifyOrderInput): boolean {
const noChanges =
!input.adjustOrderLines?.length &&
Expand All @@ -665,7 +735,8 @@ export class OrderModifier {
!input.updateShippingAddress &&
!input.updateBillingAddress &&
!input.couponCodes &&
!(input as any).customFields;
!(input as any).customFields &&
(!input.shippingMethodIds || input.shippingMethodIds.length === 0);
return noChanges;
}

Expand Down
Loading

0 comments on commit 400d78a

Please sign in to comment.