diff --git a/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.graphql.liquid b/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.graphql.liquid index 12a8d131..e96e4b65 100644 --- a/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.graphql.liquid +++ b/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.graphql.liquid @@ -2,6 +2,9 @@ query DeliveryInput { cart { deliveryGroups { id + deliveryOptions { + deliveryMethodType + } } } discount { diff --git a/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.liquid b/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.liquid index c8ee0d4c..fcb80257 100644 --- a/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.liquid +++ b/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.liquid @@ -1,6 +1,7 @@ {%- if flavor contains "vanilla-js" -%} import { DeliveryDiscountSelectionStrategy, + DeliveryMethod, DiscountClass, } from "../generated/api"; @@ -15,11 +16,6 @@ import { */ export function cartDeliveryOptionsDiscountsGenerateRun(input) { - const firstDeliveryGroup = input.cart.deliveryGroups[0]; - if (!firstDeliveryGroup) { - throw new Error("No delivery groups found"); - } - const hasShippingDiscountClass = input.discount.discountClasses.includes( DiscountClass.Shipping, ); @@ -28,6 +24,21 @@ export function cartDeliveryOptionsDiscountsGenerateRun(input) { return {operations: []}; } + // Discount only the groups with shipping options. + const targets = input.cart.deliveryGroups + // If you also want to apply a discount for other delivery methods (e.g. LOCAL), ensure to account for that here. + .filter(({deliveryOptions}) => + deliveryOptions.some( + ({deliveryMethodType}) => + deliveryMethodType === DeliveryMethod.Shipping, + ), + ) + .map(({id}) => ({deliveryGroup: {id}})); + + if (targets.length === 0) { + return {operations: []}; + } + return { operations: [ { @@ -35,13 +46,7 @@ export function cartDeliveryOptionsDiscountsGenerateRun(input) { candidates: [ { message: "FREE DELIVERY", - targets: [ - { - deliveryGroup: { - id: firstDeliveryGroup.id, - }, - }, - ], + targets, value: { percentage: { value: 100, @@ -58,6 +63,7 @@ export function cartDeliveryOptionsDiscountsGenerateRun(input) { {%- elsif flavor contains "typescript" -%} import { DeliveryDiscountSelectionStrategy, + DeliveryMethod, DiscountClass, DeliveryInput, CartDeliveryOptionsDiscountsGenerateRunResult, @@ -66,11 +72,6 @@ import { export function cartDeliveryOptionsDiscountsGenerateRun( input: DeliveryInput, ): CartDeliveryOptionsDiscountsGenerateRunResult { - const firstDeliveryGroup = input.cart.deliveryGroups[0]; - if (!firstDeliveryGroup) { - throw new Error("No delivery groups found"); - } - const hasShippingDiscountClass = input.discount.discountClasses.includes( DiscountClass.Shipping, ); @@ -79,6 +80,21 @@ export function cartDeliveryOptionsDiscountsGenerateRun( return {operations: []}; } + // Discount only the groups with shipping options. + const targets = input.cart.deliveryGroups + // If you also want to apply a discount for other delivery methods (e.g. LOCAL), ensure to account for that here. + .filter(({deliveryOptions}) => + deliveryOptions.some( + ({deliveryMethodType}) => + deliveryMethodType === DeliveryMethod.Shipping, + ), + ) + .map(({id}) => ({deliveryGroup: {id}})); + + if (targets.length === 0) { + return {operations: []}; + } + return { operations: [ { @@ -86,13 +102,7 @@ export function cartDeliveryOptionsDiscountsGenerateRun( candidates: [ { message: "FREE DELIVERY", - targets: [ - { - deliveryGroup: { - id: firstDeliveryGroup.id, - }, - }, - ], + targets, value: { percentage: { value: 100, diff --git a/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.test.liquid b/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.test.liquid index a34f21ae..4e3b07f4 100644 --- a/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.test.liquid +++ b/discounts/javascript/discount/default/src/cart_delivery_options_discounts_generate_run.test.liquid @@ -4,6 +4,7 @@ import {describe, it, expect} from "vitest"; import {cartDeliveryOptionsDiscountsGenerateRun} from "./cart_delivery_options_discounts_generate_run"; import { DeliveryDiscountSelectionStrategy, + DeliveryMethod, DiscountClass, } from "../generated/api"; @@ -13,53 +14,54 @@ import { */ describe("cartDeliveryOptionsDiscountsGenerateRun", () => { - const baseInput = { - cart: { - deliveryGroups: [ - { - id: "gid://shopify/DeliveryGroup/0", - }, - ], - }, - discount: { - discountClasses: [], - }, - }; + /** + * @param {string} id + * @param {DeliveryMethod} deliveryMethodType + * @returns {DeliveryInput["cart"]["deliveryGroups"][number]} + */ + function deliveryGroup(id, deliveryMethodType) { + return {id, deliveryOptions: [{deliveryMethodType}]}; + } - it("returns empty operations when no discount classes are present", () => { - const input = { - ...baseInput, - discount: { - discountClasses: [], - }, - }; + const pickupGroup = deliveryGroup( + "gid://shopify/CartDeliveryGroup/0", + DeliveryMethod.PickUp, + ); + const shippingGroup = deliveryGroup( + "gid://shopify/CartDeliveryGroup/1", + DeliveryMethod.Shipping, + ); - const result = cartDeliveryOptionsDiscountsGenerateRun(input); - expect(result.operations).toHaveLength(0); - }); - - it("returns delivery discount when shipping discount class is present", () => { - const input = { - ...baseInput, + /** + * @param {DeliveryInput["cart"]["deliveryGroups"]} deliveryGroups + * @param {DiscountClass[]} discountClasses + * @returns {DeliveryInput} + */ + function inputWith( + deliveryGroups, + discountClasses = [DiscountClass.Shipping], + ) { + return { + cart: { + deliveryGroups, + }, discount: { - discountClasses: [DiscountClass.Shipping], + discountClasses, }, }; + } - const result = cartDeliveryOptionsDiscountsGenerateRun(input); - expect(result.operations).toHaveLength(1); - expect(result.operations[0]).toMatchObject({ + /** + * @param {string[]} groupIds + * @returns {CartDeliveryOptionsDiscountsGenerateRunResult["operations"][number]} + */ + function freeDeliveryOperation(groupIds) { + return { deliveryDiscountsAdd: { candidates: [ { message: "FREE DELIVERY", - targets: [ - { - deliveryGroup: { - id: "gid://shopify/DeliveryGroup/0", - }, - }, - ], + targets: groupIds.map((id) => ({deliveryGroup: {id}})), value: { percentage: { value: 100, @@ -69,22 +71,69 @@ describe("cartDeliveryOptionsDiscountsGenerateRun", () => { ], selectionStrategy: DeliveryDiscountSelectionStrategy.All, }, - }); + }; + } + + it("returns empty operations when no discount classes are present", () => { + const result = cartDeliveryOptionsDiscountsGenerateRun( + inputWith([shippingGroup], []), + ); + expect(result.operations).toEqual([]); }); - it("throws error when no delivery groups are present", () => { - const input = { - cart: { - deliveryGroups: [], - }, - discount: { - discountClasses: [DiscountClass.Shipping], - }, - }; + it("returns delivery discount when shipping discount class is present", () => { + const result = cartDeliveryOptionsDiscountsGenerateRun( + inputWith([shippingGroup]), + ); + expect(result.operations).toEqual([ + freeDeliveryOperation(["gid://shopify/CartDeliveryGroup/1"]), + ]); + }); + + it("discounts only the shipping group when the pickup group is first in a split", () => { + const result = cartDeliveryOptionsDiscountsGenerateRun( + inputWith([pickupGroup, shippingGroup]), + ); + expect(result.operations).toEqual([ + freeDeliveryOperation(["gid://shopify/CartDeliveryGroup/1"]), + ]); + }); + + it("discounts only the shipping group when the shipping group is first in a split", () => { + const result = cartDeliveryOptionsDiscountsGenerateRun( + inputWith([shippingGroup, pickupGroup]), + ); + expect(result.operations).toEqual([ + freeDeliveryOperation(["gid://shopify/CartDeliveryGroup/1"]), + ]); + }); - expect(() => cartDeliveryOptionsDiscountsGenerateRun(input)).toThrow( - "No delivery groups found", + it("returns empty operations when only a pickup group is present", () => { + const result = cartDeliveryOptionsDiscountsGenerateRun( + inputWith([pickupGroup]), ); + expect(result.operations).toEqual([]); + }); + + it("returns empty operations for local delivery and pickup point groups", () => { + const result = cartDeliveryOptionsDiscountsGenerateRun( + inputWith([ + deliveryGroup( + "gid://shopify/CartDeliveryGroup/2", + DeliveryMethod.Local, + ), + deliveryGroup( + "gid://shopify/CartDeliveryGroup/3", + DeliveryMethod.PickupPoint, + ), + ]), + ); + expect(result.operations).toEqual([]); + }); + + it("returns empty operations when no delivery groups are present", () => { + const result = cartDeliveryOptionsDiscountsGenerateRun(inputWith([])); + expect(result.operations).toEqual([]); }); }); {%- elsif flavor contains "typescript" -%} @@ -93,61 +142,52 @@ import {describe, it, expect} from "vitest"; import {cartDeliveryOptionsDiscountsGenerateRun} from "./cart_delivery_options_discounts_generate_run"; import { DeliveryDiscountSelectionStrategy, + DeliveryMethod, DiscountClass, DeliveryInput, CartDeliveryOptionsDiscountsGenerateRunResult, } from "../generated/api"; describe("cartDeliveryOptionsDiscountsGenerateRun", () => { - const baseInput: DeliveryInput = { - cart: { - deliveryGroups: [ - { - id: "gid://shopify/DeliveryGroup/0", - }, - ], - }, - discount: { - discountClasses: [], - }, - }; + function deliveryGroup( + id: string, + deliveryMethodType: DeliveryMethod, + ): DeliveryInput["cart"]["deliveryGroups"][number] { + return {id, deliveryOptions: [{deliveryMethodType}]}; + } - it("returns empty operations when no discount classes are present", () => { - const input: DeliveryInput = { - ...baseInput, - discount: { - discountClasses: [], - }, - }; + const pickupGroup = deliveryGroup( + "gid://shopify/CartDeliveryGroup/0", + DeliveryMethod.PickUp, + ); + const shippingGroup = deliveryGroup( + "gid://shopify/CartDeliveryGroup/1", + DeliveryMethod.Shipping, + ); - const result: CartDeliveryOptionsDiscountsGenerateRunResult = - cartDeliveryOptionsDiscountsGenerateRun(input); - expect(result.operations).toHaveLength(0); - }); - - it("returns delivery discount when shipping discount class is present", () => { - const input: DeliveryInput = { - ...baseInput, + function inputWith( + deliveryGroups: DeliveryInput["cart"]["deliveryGroups"], + discountClasses: DiscountClass[] = [DiscountClass.Shipping], + ): DeliveryInput { + return { + cart: { + deliveryGroups, + }, discount: { - discountClasses: [DiscountClass.Shipping], + discountClasses, }, }; + } - const result: CartDeliveryOptionsDiscountsGenerateRunResult = - cartDeliveryOptionsDiscountsGenerateRun(input); - expect(result.operations).toHaveLength(1); - expect(result.operations[0]).toMatchObject({ + function freeDeliveryOperation( + groupIds: string[], + ): CartDeliveryOptionsDiscountsGenerateRunResult["operations"][number] { + return { deliveryDiscountsAdd: { candidates: [ { message: "FREE DELIVERY", - targets: [ - { - deliveryGroup: { - id: "gid://shopify/DeliveryGroup/0", - }, - }, - ], + targets: groupIds.map((id) => ({deliveryGroup: {id}})), value: { percentage: { value: 100, @@ -157,22 +197,70 @@ describe("cartDeliveryOptionsDiscountsGenerateRun", () => { ], selectionStrategy: DeliveryDiscountSelectionStrategy.All, }, - }); + }; + } + + it("returns empty operations when no discount classes are present", () => { + const result: CartDeliveryOptionsDiscountsGenerateRunResult = + cartDeliveryOptionsDiscountsGenerateRun(inputWith([shippingGroup], [])); + expect(result.operations).toEqual([]); }); - it("throws error when no delivery groups are present", () => { - const input: DeliveryInput = { - cart: { - deliveryGroups: [], - }, - discount: { - discountClasses: [DiscountClass.Shipping], - }, - }; + it("returns delivery discount when shipping discount class is present", () => { + const result: CartDeliveryOptionsDiscountsGenerateRunResult = + cartDeliveryOptionsDiscountsGenerateRun(inputWith([shippingGroup])); + expect(result.operations).toEqual([ + freeDeliveryOperation(["gid://shopify/CartDeliveryGroup/1"]), + ]); + }); - expect(() => cartDeliveryOptionsDiscountsGenerateRun(input)).toThrow( - "No delivery groups found", - ); + it("discounts only the shipping group when the pickup group is first in a split", () => { + const result: CartDeliveryOptionsDiscountsGenerateRunResult = + cartDeliveryOptionsDiscountsGenerateRun( + inputWith([pickupGroup, shippingGroup]), + ); + expect(result.operations).toEqual([ + freeDeliveryOperation(["gid://shopify/CartDeliveryGroup/1"]), + ]); + }); + + it("discounts only the shipping group when the shipping group is first in a split", () => { + const result: CartDeliveryOptionsDiscountsGenerateRunResult = + cartDeliveryOptionsDiscountsGenerateRun( + inputWith([shippingGroup, pickupGroup]), + ); + expect(result.operations).toEqual([ + freeDeliveryOperation(["gid://shopify/CartDeliveryGroup/1"]), + ]); + }); + + it("returns empty operations when only a pickup group is present", () => { + const result: CartDeliveryOptionsDiscountsGenerateRunResult = + cartDeliveryOptionsDiscountsGenerateRun(inputWith([pickupGroup])); + expect(result.operations).toEqual([]); + }); + + it("returns empty operations for local delivery and pickup point groups", () => { + const result: CartDeliveryOptionsDiscountsGenerateRunResult = + cartDeliveryOptionsDiscountsGenerateRun( + inputWith([ + deliveryGroup( + "gid://shopify/CartDeliveryGroup/2", + DeliveryMethod.Local, + ), + deliveryGroup( + "gid://shopify/CartDeliveryGroup/3", + DeliveryMethod.PickupPoint, + ), + ]), + ); + expect(result.operations).toEqual([]); + }); + + it("returns empty operations when no delivery groups are present", () => { + const result: CartDeliveryOptionsDiscountsGenerateRunResult = + cartDeliveryOptionsDiscountsGenerateRun(inputWith([])); + expect(result.operations).toEqual([]); }); }); diff --git a/discounts/rust/discount/default/src/cart_delivery_options_discounts_generate_run.graphql.liquid b/discounts/rust/discount/default/src/cart_delivery_options_discounts_generate_run.graphql.liquid index 71c204a7..40ded025 100644 --- a/discounts/rust/discount/default/src/cart_delivery_options_discounts_generate_run.graphql.liquid +++ b/discounts/rust/discount/default/src/cart_delivery_options_discounts_generate_run.graphql.liquid @@ -2,6 +2,9 @@ query Input { cart { deliveryGroups { id + deliveryOptions { + deliveryMethodType + } } } discount { diff --git a/discounts/rust/discount/default/src/cart_delivery_options_discounts_generate_run.rs b/discounts/rust/discount/default/src/cart_delivery_options_discounts_generate_run.rs index 2e622dd4..d642f08a 100644 --- a/discounts/rust/discount/default/src/cart_delivery_options_discounts_generate_run.rs +++ b/discounts/rust/discount/default/src/cart_delivery_options_discounts_generate_run.rs @@ -16,22 +16,34 @@ fn cart_delivery_options_discounts_generate_run( return Ok(output::CartDeliveryOptionsDiscountsGenerateRunResult { operations: vec![] }); } - let first_delivery_group = input + // Discount only the groups with shipping options. + let targets: Vec = input .cart .delivery_groups - .first() - .ok_or("No delivery groups found")?; + .iter() + // If you also want to apply a discount for other delivery methods (e.g. LOCAL), ensure to account for that here. + .filter(|group| { + group.delivery_options.iter().any(|option| { + matches!(option.delivery_method_type, input::DeliveryMethod::SHIPPING) + }) + }) + .map(|group| { + output::DeliveryDiscountCandidateTarget::DeliveryGroup(output::DeliveryGroupTarget { + id: group.id.clone(), + }) + }) + .collect(); + + if targets.is_empty() { + return Ok(output::CartDeliveryOptionsDiscountsGenerateRunResult { operations: vec![] }); + } Ok(output::CartDeliveryOptionsDiscountsGenerateRunResult { operations: vec![output::DeliveryOperation::DeliveryDiscountsAdd( output::DeliveryDiscountsAddOperation { selection_strategy: output::DeliveryDiscountSelectionStrategy::ALL, candidates: vec![output::DeliveryDiscountCandidate { - targets: vec![output::DeliveryDiscountCandidateTarget::DeliveryGroup( - output::DeliveryGroupTarget { - id: first_delivery_group.id.clone(), - }, - )], + targets, value: output::DeliveryDiscountCandidateValue::Percentage(output::Percentage { value: Decimal(100.0), }), @@ -42,3 +54,120 @@ fn cart_delivery_options_discounts_generate_run( )], }) } + +#[cfg(test)] +mod tests { + use super::*; + use cart_delivery_options_discounts_generate_run::output; + use shopify_function::run_function_with_input; + + const PICKUP_GROUP: &str = r#"{ + "id": "gid://shopify/CartDeliveryGroup/0", + "deliveryOptions": [{ "deliveryMethodType": "PICK_UP" }] + }"#; + const SHIPPING_GROUP: &str = r#"{ + "id": "gid://shopify/CartDeliveryGroup/1", + "deliveryOptions": [{ "deliveryMethodType": "SHIPPING" }] + }"#; + const LOCAL_GROUP: &str = r#"{ + "id": "gid://shopify/CartDeliveryGroup/2", + "deliveryOptions": [{ "deliveryMethodType": "LOCAL" }] + }"#; + const PICKUP_POINT_GROUP: &str = r#"{ + "id": "gid://shopify/CartDeliveryGroup/3", + "deliveryOptions": [{ "deliveryMethodType": "PICKUP_POINT" }] + }"#; + + fn run( + delivery_groups: &[&str], + discount_classes: &str, + ) -> Result> { + let input = format!( + r#"{{ + "cart": {{ "deliveryGroups": [{}] }}, + "discount": {{ "discountClasses": {} }} + }}"#, + delivery_groups.join(","), + discount_classes + ); + let result = run_function_with_input(cart_delivery_options_discounts_generate_run, &input)?; + Ok(result.operations) + } + + fn free_delivery_operations(group_ids: &[&str]) -> Vec { + vec![output::DeliveryOperation::DeliveryDiscountsAdd( + output::DeliveryDiscountsAddOperation { + selection_strategy: output::DeliveryDiscountSelectionStrategy::ALL, + candidates: vec![output::DeliveryDiscountCandidate { + targets: group_ids + .iter() + .map(|id| { + output::DeliveryDiscountCandidateTarget::DeliveryGroup( + output::DeliveryGroupTarget { id: id.to_string() }, + ) + }) + .collect(), + value: output::DeliveryDiscountCandidateValue::Percentage(output::Percentage { + value: Decimal(100.0), + }), + message: Some("FREE DELIVERY".to_string()), + associated_discount_code: None, + }], + }, + )] + } + + #[test] + fn returns_no_operations_without_the_shipping_discount_class() -> Result<()> { + assert_eq!(run(&[SHIPPING_GROUP], "[]")?, vec![]); + Ok(()) + } + + #[test] + fn discounts_the_delivery_group() -> Result<()> { + assert_eq!( + run(&[SHIPPING_GROUP], r#"["SHIPPING"]"#)?, + free_delivery_operations(&["gid://shopify/CartDeliveryGroup/1"]) + ); + Ok(()) + } + + #[test] + fn discounts_only_the_shipping_group_when_pickup_is_first() -> Result<()> { + assert_eq!( + run(&[PICKUP_GROUP, SHIPPING_GROUP], r#"["SHIPPING"]"#)?, + free_delivery_operations(&["gid://shopify/CartDeliveryGroup/1"]) + ); + Ok(()) + } + + #[test] + fn discounts_only_the_shipping_group_when_shipping_is_first() -> Result<()> { + assert_eq!( + run(&[SHIPPING_GROUP, PICKUP_GROUP], r#"["SHIPPING"]"#)?, + free_delivery_operations(&["gid://shopify/CartDeliveryGroup/1"]) + ); + Ok(()) + } + + #[test] + fn returns_no_operations_for_only_a_pickup_group() -> Result<()> { + assert_eq!(run(&[PICKUP_GROUP], r#"["SHIPPING"]"#)?, vec![]); + Ok(()) + } + + #[test] + fn returns_no_operations_for_local_delivery_and_pickup_point_groups() -> Result<()> { + assert_eq!( + run(&[LOCAL_GROUP, PICKUP_POINT_GROUP], r#"["SHIPPING"]"#)?, + vec![] + ); + Ok(()) + } + + #[test] + fn returns_no_operations_without_delivery_groups() -> Result<()> { + assert_eq!(run(&[], r#"["SHIPPING"]"#)?, vec![]); + Ok(()) + } +}