From 5838b9798dc7b1492435c8cfc840c2a0f3fc903a Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Wed, 16 Sep 2026 16:15:33 +0100 Subject: [PATCH 1/5] Update accelerated checkouts to Storefront API 2026-07 --- .../Internal/StorefrontAPI/StorefrontAPI+Types.swift | 2 +- .../Internal/StorefrontAPI/StorefrontAPI.swift | 2 +- .../ShopifyAcceleratedCheckouts.swift | 2 +- .../ShopifyAcceleratedCheckoutsTests.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift index fd2ddeb8b..b5e95c3cd 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift @@ -906,7 +906,7 @@ extension StorefrontAPI { } /// Represents shop settings data fetched from the Storefront API -/// https://shopify.dev/docs/api/storefront/2026-04/objects/Shop +/// https://shopify.dev/docs/api/storefront/2026-07/objects/Shop @available(iOS 16.0, *) class ShopSettings: ObservableObject { /// The shop's name (merchant name for display) diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI.swift index 4d48720b3..cfbab94ee 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI.swift @@ -9,7 +9,7 @@ final class StorefrontAPI: ObservableObject, StorefrontAPIProtocol { /// - Parameters: /// - storefrontDomain: The shop domain (e.g., "example.myshopify.com") /// - storefrontAccessToken: The storefront access token - /// - apiVersion: The API version to use (defaults to "2026-04") + /// - apiVersion: The API version to use (defaults to "2026-07") /// - countryCode: Optional country code for localization /// - languageCode: Optional language code for localization init( diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift index 97b0c2cb3..d3583446d 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift @@ -2,7 +2,7 @@ import ShopifyCheckoutKit public enum ShopifyAcceleratedCheckouts { /// Storefront API version used for cart operations - internal static let apiVersion = "2026-04" + internal static let apiVersion = "2026-07" internal static let name = "ShopifyAcceleratedCheckouts" diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/ShopifyAcceleratedCheckoutsTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/ShopifyAcceleratedCheckoutsTests.swift index 53ebccc32..4428d277b 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/ShopifyAcceleratedCheckoutsTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/ShopifyAcceleratedCheckoutsTests.swift @@ -19,7 +19,7 @@ class ShopifyAcceleratedCheckoutsTests: XCTestCase { } func test_apiVersion_whenAccessed_shouldBePublic() { - XCTAssertEqual(ShopifyAcceleratedCheckouts.apiVersion, "2026-04") + XCTAssertEqual(ShopifyAcceleratedCheckouts.apiVersion, "2026-07") } func test_logLevel_withDefaultConfiguration_shouldDefaultToWarn() { From 0b6da87ae535a708d6b1977c22ade5952c694131 Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Wed, 16 Sep 2026 16:32:55 +0100 Subject: [PATCH 2/5] Migrate accelerated checkout discounts to cart applications --- .../GraphQLDocument+Fragments.swift | 140 +----------- .../StorefrontAPI/StorefrontAPI+Types.swift | 131 +---------- .../ApplePay/Data/PassKitFactory.swift | 103 +-------- .../StorefrontAPIMutationsTests.swift | 45 ++-- .../StorefrontAPIQueriesTests.swift | 55 ++++- .../TestHelpers.swift | 9 +- ...AuthorizationDelegateControllerTests.swift | 4 +- .../ApplePayAuthorizationDelegateTests.swift | 3 +- .../ErrorHandler/ErrorHandlerTests.swift | 3 +- .../ErrorHandler_WarningsTests.swift | 6 +- .../Wallets/ApplePay/PKDecoderTests.swift | 3 +- .../ApplePay/PassKitFactoryTests.swift | 205 ++++++++++++++++++ 12 files changed, 287 insertions(+), 420 deletions(-) diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift index 79e17de61..fe150e79a 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift @@ -60,75 +60,14 @@ extension GraphQLDocument { currencyCode } } - discountCodes { - applicable - code - } - discountAllocations { - __typename - ... on CartAutomaticDiscountAllocation { - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } + discountApplications { + targetType + totalAllocatedAmount { + amount + currencyCode } - ... on CartCodeDiscountAllocation { + ... on CartCodeDiscountApplication { code - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } - } - ... on CartCustomDiscountAllocation { - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } } } } @@ -195,73 +134,6 @@ extension GraphQLDocument { currencyCode } } - discountAllocations { - __typename - ... on CartAutomaticDiscountAllocation { - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } - } - ... on CartCodeDiscountAllocation { - code - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } - } - ... on CartCustomDiscountAllocation { - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } - } - } } """ diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift index b5e95c3cd..7a684184f 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift @@ -27,14 +27,7 @@ extension StorefrontAPI { let delivery: CartDelivery? let lines: BaseCartLineConnection let cost: CartCost - let discountCodes: [CartDiscountCode] - let discountAllocations: [CartDiscountAllocation] - } - - /// Cart discount code - struct CartDiscountCode: Codable { - let code: String - let applicable: Bool + let discountApplications: [CartDiscountApplication] } /// Cart buyer identity @@ -136,85 +129,11 @@ extension StorefrontAPI { let nodes: [BaseCartLine] } - /// Cart discount allocation - enum CartDiscountAllocation: Codable { - case automatic(CartAutomaticDiscountAllocation) - case code(CartCodeDiscountAllocation) - case custom(CartCustomDiscountAllocation) - - private enum CodingKeys: String, CodingKey { - case __typename - } - - private enum TypeName: String, Codable { - case cartAutomaticDiscountAllocation = "CartAutomaticDiscountAllocation" - case cartCodeDiscountAllocation = "CartCodeDiscountAllocation" - case cartCustomDiscountAllocation = "CartCustomDiscountAllocation" - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let typename = try container.decode(TypeName.self, forKey: .__typename) - - switch typename { - case .cartAutomaticDiscountAllocation: - let allocation = try CartAutomaticDiscountAllocation(from: decoder) - self = .automatic(allocation) - case .cartCodeDiscountAllocation: - let allocation = try CartCodeDiscountAllocation(from: decoder) - self = .code(allocation) - case .cartCustomDiscountAllocation: - let allocation = try CartCustomDiscountAllocation(from: decoder) - self = .custom(allocation) - } - } - - func encode(to encoder: Encoder) throws { - switch self { - case let .automatic(allocation): - try allocation.encode(to: encoder) - case let .code(allocation): - try allocation.encode(to: encoder) - case let .custom(allocation): - try allocation.encode(to: encoder) - } - } - } - - /// Automatic discount allocation - struct CartAutomaticDiscountAllocation: Codable { - let discountApplication: CartDiscountApplication - let discountedAmount: MoneyV2 - let targetType: DiscountApplicationTargetType - } - - /// Code discount allocation - struct CartCodeDiscountAllocation: Codable { - let code: String - let discountApplication: CartDiscountApplication - let discountedAmount: MoneyV2 - let targetType: DiscountApplicationTargetType - } - - /// Custom discount allocation - struct CartCustomDiscountAllocation: Codable { - let discountApplication: CartDiscountApplication - let discountedAmount: MoneyV2 - let targetType: DiscountApplicationTargetType - } - /// Cart discount application struct CartDiscountApplication: Codable { - let targetSelection: DiscountApplicationTargetSelection let targetType: DiscountApplicationTargetType - let value: PricingValue - } - - /// Discount application target selection - enum DiscountApplicationTargetSelection: String, Codable { - case all = "ALL" - case entitled = "ENTITLED" - case explicit = "EXPLICIT" + let totalAllocatedAmount: MoneyV2 + let code: String? } /// Discount application target type @@ -223,56 +142,12 @@ extension StorefrontAPI { case shippingLine = "SHIPPING_LINE" } - /// Pricing value (union type for percentage or fixed amount) - enum PricingValue: Codable { - case percentage(PricingPercentageValue) - case fixedAmount(MoneyV2) - - private enum CodingKeys: String, CodingKey { - case __typename - } - - private enum TypeName: String, Codable { - case pricingPercentageValue = "PricingPercentageValue" - case moneyV2 = "MoneyV2" - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let typename = try container.decode(TypeName.self, forKey: .__typename) - - switch typename { - case .pricingPercentageValue: - let percentage = try PricingPercentageValue(from: decoder) - self = .percentage(percentage) - case .moneyV2: - let amount = try MoneyV2(from: decoder) - self = .fixedAmount(amount) - } - } - - func encode(to encoder: Encoder) throws { - switch self { - case let .percentage(percentage): - try percentage.encode(to: encoder) - case let .fixedAmount(amount): - try amount.encode(to: encoder) - } - } - } - - /// Pricing percentage value - struct PricingPercentageValue: Codable { - let percentage: Double - } - /// Base cart line struct BaseCartLine: Codable { let id: Types.ID let quantity: Int let merchandise: ProductVariant? let cost: CartLineCost - let discountAllocations: [CartDiscountAllocation] } /// Cart line cost diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift index 137de8f6e..e91ff7027 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift @@ -103,8 +103,7 @@ final class PassKitFactory: Sendable { } } - /// Gets all discount allocations for line items, including applicable discount codes - /// that are not already accounted for in cart or line item allocations. + /// Gets the allocated totals from cart discount applications for display. /// /// - Parameter cart: The cart to get discount allocations from /// - Returns: An array of DiscountAllocationInfo representing all discounts @@ -116,107 +115,15 @@ final class PassKitFactory: Sendable { throw ShopifyAcceleratedCheckouts.Error.invariant(expected: "cart") } - let currencyCode = cart.cost.totalAmount.currencyCode - - // Get all discount allocations from line items - let lineItemDiscountAllocations = cart.lines.nodes.flatMap { $0.discountAllocations } - - // Find discount codes that are applicable but not already accounted for - let applicableOtherDiscountCodes = findApplicableUnaccountedDiscountCodes( - cart: cart, - lineItemDiscountAllocations: lineItemDiscountAllocations - ) - - // Map applicable discount codes to synthetic discount allocations with 0 amount - // These are typically shipping discounts that don't show up in other allocations - let shippingDiscounts = createShippingDiscounts( - discountCodes: applicableOtherDiscountCodes, - currencyCode: currencyCode - ) - - // Convert cart discount allocations - let cartDiscounts = convertDiscountAllocations(cart.discountAllocations) - - // Convert line item discount allocations - let productDiscounts = convertDiscountAllocations(lineItemDiscountAllocations) - - // Return combined array: shipping discounts + cart discounts + product discounts - return shippingDiscounts + cartDiscounts + productDiscounts - } - - /// Finds discount codes that are applicable but not already accounted for in allocations - private func findApplicableUnaccountedDiscountCodes( - cart: StorefrontAPI.Cart, - lineItemDiscountAllocations: [StorefrontAPI.CartDiscountAllocation] - ) -> [StorefrontAPI.CartDiscountCode] { - return cart.discountCodes.filter { discountCode in - guard discountCode.applicable else { return false } - - // Check if not in cart discount allocations - let allocations = - (lineItemDiscountAllocations + cart.discountAllocations) - .contains { allocation in - if case let .code(codeAllocation) = allocation { - return codeAllocation.code == discountCode.code - } - return false - } - - return !allocations - } - } - - /// Creates shipping discount allocations with 0 amount for applicable discount codes - private func createShippingDiscounts( - discountCodes: [StorefrontAPI.CartDiscountCode], - currencyCode: String - ) -> [DiscountAllocationInfo] { - return discountCodes.map { discountCode in + return cart.discountApplications.map { application in DiscountAllocationInfo( - code: discountCode.code, - amount: 0, - currencyCode: currencyCode + code: application.code, + amount: application.totalAllocatedAmount.amount, + currencyCode: application.totalAllocatedAmount.currencyCode ) } } - /// Converts cart discount allocations to DiscountAllocationInfo - private func convertDiscountAllocations( - _ allocations: [StorefrontAPI.CartDiscountAllocation] - ) -> [DiscountAllocationInfo] { - return allocations.compactMap { allocation in - convertSingleDiscountAllocation(allocation) - } - } - - /// Converts a single discount allocation to DiscountAllocationInfo - private func convertSingleDiscountAllocation( - _ allocation: StorefrontAPI.CartDiscountAllocation - ) -> DiscountAllocationInfo { - let code: String? = - if case let .code(codeAllocation) = allocation { - codeAllocation.code - } else { - nil - } - - let (discountedAmount, currencyCode) = - switch allocation { - case let .automatic(auto): - (auto.discountedAmount.amount, auto.discountedAmount.currencyCode) - case let .code(code): - (code.discountedAmount.amount, code.discountedAmount.currencyCode) - case let .custom(custom): - (custom.discountedAmount.amount, custom.discountedAmount.currencyCode) - } - - return DiscountAllocationInfo( - code: code, - amount: discountedAmount, - currencyCode: currencyCode - ) - } - /// Computes the cartesian product of a 2D array. The cartesian product is the set of all possible /// ordered combinations of elements from the input arrays. /// diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift index 1383ed0c9..408a18f7c 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift @@ -113,8 +113,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "cost": { "totalAmount": {"amount": "19.99", "currencyCode": "USD"}, "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"} - }, - "discountAllocations": [] + } }] }, "cost": { @@ -122,8 +121,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -157,8 +155,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "0.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -217,8 +214,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": null, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -272,8 +268,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -334,8 +329,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -400,8 +394,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "29.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -480,8 +473,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -654,8 +646,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -695,8 +686,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": {"amount": "2.50", "currencyCode": "USD"} }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -829,8 +819,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": {"amount": "3.00", "currencyCode": "USD"} }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -899,8 +888,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -936,8 +924,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": {"amount": "2.50", "currencyCode": "USD"} }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "checkoutURL": "https://test.myshopify.com/checkout/ready-123" }, @@ -1319,8 +1306,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "0.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [{ "field": ["input"], @@ -1364,8 +1350,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "0.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [{ "field": ["buyerIdentity", "email"], diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift index 94be7c0c9..dd4891743 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift @@ -84,8 +84,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "cost": { "totalAmount": {"amount": "39.98", "currencyCode": "USD"}, "subtotalAmount": {"amount": "39.98", "currencyCode": "USD"} - }, - "discountAllocations": [] + } }] }, "cost": { @@ -93,8 +92,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": {"amount": "39.98", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] } } } @@ -145,8 +143,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": null, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] } } } @@ -279,8 +276,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": null, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] } }, "errors": [ @@ -377,8 +373,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "cost": { "totalAmount": {"amount": "10.00", "currencyCode": "USD"}, "subtotalAmount": {"amount": "10.00", "currencyCode": "USD"} - }, - "discountAllocations": [] + } } """) } @@ -401,8 +396,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": {"amount": "1000.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] } } } @@ -447,4 +441,41 @@ final class StorefrontAPIQueriesTests: XCTestCase { XCTFail("Failed to parse request body: \(error)") } } + + func testCartQueryDecodesDiscountApplicationsWithoutLegacyAllocations() async throws { + mockJSONResponse(""" + {"data":{"cart":{ + "id":"gid://shopify/Cart/discounts", + "checkoutUrl":"https://test.myshopify.com/checkout/discounts", + "totalQuantity":0, + "deliveryGroups":{"nodes":[]}, + "lines":{"nodes":[]}, + "cost":{"totalAmount":{"amount":"0.00","currencyCode":"USD"}}, + "discountApplications":[ + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"5.25","currencyCode":"USD"},"code":"SAVE"}, + {"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"3.00","currencyCode":"USD"}} + ] + }}} + """) + + let cart = try await storefrontAPI.cart(by: GraphQLScalars.ID("gid://shopify/Cart/discounts")) + let discounts = try XCTUnwrap(cart?.discountApplications) + + XCTAssertEqual(discounts.count, 2) + XCTAssertEqual(discounts.first?.code, "SAVE") + XCTAssertEqual(discounts.first?.totalAllocatedAmount.amount, Decimal(string: "5.25")) + XCTAssertEqual(discounts.first?.targetType, .lineItem) + XCTAssertNil(discounts.last?.code) + XCTAssertEqual(discounts.last?.targetType, .shippingLine) + + let requestBody = try XCTUnwrap(MockURLProtocol.capturedRequestBody) + let body = try XCTUnwrap(JSONSerialization.jsonObject(with: requestBody) as? [String: Any]) + let query = try XCTUnwrap(body["query"] as? String) + + XCTAssertTrue(query.contains("discountApplications {")) + XCTAssertTrue(query.contains("totalAllocatedAmount {")) + XCTAssertTrue(query.contains("... on CartCodeDiscountApplication")) + XCTAssertFalse(query.contains("discountAllocations")) + XCTAssertFalse(query.contains("sourceDiscountApplication")) + } } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift index e33406614..7241bcdb1 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift @@ -184,8 +184,7 @@ extension StorefrontAPI.Cart { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } @@ -213,8 +212,7 @@ extension StorefrontAPI.Cart { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } @@ -265,8 +263,7 @@ extension StorefrontAPI.Cart { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift index c7ed15aa2..3794695c6 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift @@ -587,7 +587,7 @@ final class ApplePayAuthorizationDelegateControllerTests: XCTestCase { "\"delivery\":null," + "\"lines\":{\"nodes\":[]}," + "\"cost\":{\"totalAmount\":{\"amount\":\"0.00\",\"currencyCode\":\"USD\"}}," + - "\"discountCodes\":[],\"discountAllocations\":[]}" + "\"discountApplications\":[]}" static let mockCartWithAddressResponse: String = "{" + "\"id\":\"gid://shopify/Cart/test\"," + @@ -610,7 +610,7 @@ final class ApplePayAuthorizationDelegateControllerTests: XCTestCase { "\"delivery\":{\"addresses\":[{\"id\":\"gid://shopify/CartSelectableAddress/1\",\"selected\":true,\"address\":{\"countryCode\":\"US\"}}]}," + "\"lines\":{\"nodes\":[]}," + "\"cost\":{\"totalAmount\":{\"amount\":\"0.00\",\"currencyCode\":\"USD\"}}," + - "\"discountCodes\":[],\"discountAllocations\":[]}" + "\"discountApplications\":[]}" private struct State { var failReplace = false diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift index e9d5c312e..f09bb4689 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift @@ -639,8 +639,7 @@ final class ApplePayAuthorizationDelegateTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift index 2ae6986fe..6aec7c8cd 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift @@ -207,8 +207,7 @@ class ErrorHandlerTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift index 914c02dfc..8ffa1226d 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift @@ -19,8 +19,7 @@ class ErrorHandler_WarningsTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) let result = ErrorHandler.map(warningType: StorefrontAPI.WarningType.outOfStock, cart: cart) @@ -50,8 +49,7 @@ class ErrorHandler_WarningsTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) let result = ErrorHandler.map(warningType: StorefrontAPI.WarningType.notEnoughStock, cart: cart) diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift index 54244897f..7f6f0125a 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift @@ -38,8 +38,7 @@ class PKDecoderTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift index 95f0ae1e6..cbfe01325 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift @@ -108,4 +108,209 @@ class PassKitFactoryTests: XCTestCase { XCTAssertEqual(secondCombination?.amount, NSDecimalNumber(decimal: 25.99)) XCTAssertEqual(secondCombination?.detail, "1 to 2 days and 5 to 7 days") } + + func testFixedDiscountAcrossMultipleLinesUsesAllocatedTotalOnce() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"7.00","currencyCode":"USD"},"code":"SAVE7"}] + """, + lineSubtotals: ["30.00", "70.00"], + totalAmount: "93.00" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.first?.amount, NSDecimalNumber(string: "100.00")) + XCTAssertEqual(items.filter { $0.label == "SAVE7" }.count, 1) + XCTAssertEqual(items.first { $0.label == "SAVE7" }?.amount, NSDecimalNumber(string: "-7.00")) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "93.00")) + XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 93) + } + + func testPercentageDiscountUsesAllocatedMoneyAmount() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"17.49","currencyCode":"USD"},"code":"PERCENT15"}] + """, + lineSubtotals: ["116.60"], + totalAmount: "99.11" + ) + + let discounts = try PassKitFactory.shared.createDiscountAllocations(cart: cart) + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(discounts.count, 1) + XCTAssertEqual(discounts.first?.amount, Decimal(string: "17.49")) + XCTAssertEqual(discounts.first?.currencyCode, "USD") + XCTAssertEqual(items.first { $0.label == "PERCENT15" }?.amount, NSDecimalNumber(string: "-17.49")) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "99.11")) + } + + func testAutomaticAndCustomDiscountsShareGeneralLabelAndPreserveCodeLabels() throws { + let cart = try makeDiscountCart( + applications: """ + [ + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"4.00","currencyCode":"USD"}}, + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"1.50","currencyCode":"USD"}}, + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"3.00","currencyCode":"USD"},"code":"SAVE"}, + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"2.00","currencyCode":"USD"},"code":"SAVE"} + ] + """, + totalAmount: "89.50" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.first { $0.label == "order_summary.discount".localizedString }?.amount, NSDecimalNumber(string: "-5.50")) + XCTAssertEqual(items.first { $0.label == "SAVE" }?.amount, NSDecimalNumber(string: "-5.00")) + XCTAssertEqual(items.filter { $0.amount.decimalValue < 0 }.count, 2) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "89.50")) + } + + func testZeroAllocationDoesNotAddDiscountSummaryRow() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"0.00","currencyCode":"USD"},"code":"ZERO"}] + """, + totalAmount: "100.00" + ) + + let discounts = try PassKitFactory.shared.createDiscountAllocations(cart: cart) + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(discounts.first?.amount, 0) + XCTAssertFalse(items.contains { $0.label == "ZERO" }) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "100.00")) + } + + func testEmptyApplicationsDoNotAddDiscounts() throws { + let cart = try makeDiscountCart( + applications: "[]", + totalAmount: "100.00" + ) + + XCTAssertTrue(try PassKitFactory.shared.createDiscountAllocations(cart: cart).isEmpty) + } + + func testSummaryUsesAuthoritativeCartTotal() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"10.00","currencyCode":"USD"},"code":"SAVE10"}] + """, + totalAmount: "95.25" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.last?.label, "Test Shop") + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "95.25")) + } + + func testFreeShippingDeductsShippingApplicationOnce() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"10.00","currencyCode":"USD"},"code":"FREESHIP"}] + """, + shippingGroups: [("10.00", false)], + totalAmount: "100.00" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.first { $0.label == "order_summary.shipping".localizedString }?.amount, NSDecimalNumber(string: "10.00")) + XCTAssertEqual(items.first { $0.label == "FREESHIP" }?.amount, NSDecimalNumber(string: "-10.00")) + XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 100) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "100.00")) + } + + func testPartialShippingDiscountPreservesEstimatedShippingCost() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"4.00","currencyCode":"USD"},"code":"SHIP4"}] + """, + shippingGroups: [("10.00", false)], + totalAmount: "106.00" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + let shippingMethods = PassKitFactory.shared.createShippingMethods(deliveryGroups: cart.deliveryGroups.nodes) + + XCTAssertEqual(items.first { $0.label == "order_summary.shipping".localizedString }?.amount, NSDecimalNumber(string: "10.00")) + XCTAssertEqual(items.first { $0.label == "SHIP4" }?.amount, NSDecimalNumber(string: "-4.00")) + XCTAssertEqual(shippingMethods.first?.amount, NSDecimalNumber(string: "10.00")) + XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 106) + } + + func testCombinedProductAndShippingCodeAcrossDeliveryGroupsCountsEachApplicationOnce() throws { + let cart = try makeDiscountCart( + applications: """ + [ + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"5.00","currencyCode":"USD"},"code":"COMBINED"}, + {"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"4.00","currencyCode":"USD"},"code":"COMBINED"} + ] + """, + shippingGroups: [("8.00", false), ("2.00", true)], + totalAmount: "101.00" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.first { $0.label == "order_summary.shipping_one_time_purchase".localizedString }?.amount, NSDecimalNumber(string: "8.00")) + XCTAssertEqual(items.first { $0.label == "order_summary.shipping_subscription".localizedString }?.amount, NSDecimalNumber(string: "2.00")) + XCTAssertEqual(items.filter { $0.label == "COMBINED" }.count, 1) + XCTAssertEqual(items.first { $0.label == "COMBINED" }?.amount, NSDecimalNumber(string: "-9.00")) + XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 101) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "101.00")) + } + + private func makeDiscountCart( + applications: String, + lineSubtotals: [String] = ["100.00"], + shippingGroups: [(amount: String, subscription: Bool)] = [], + totalAmount: String + ) throws -> StorefrontAPI.Cart { + let lines = lineSubtotals.enumerated().map { index, subtotal in + """ + { + "id":"gid://shopify/CartLine/\(index)", + "quantity":2, + "merchandise":null, + "cost":{ + "totalAmount":{"amount":"\(subtotal)","currencyCode":"USD"}, + "subtotalAmount":{"amount":"\(subtotal)","currencyCode":"USD"} + } + } + """ + } + let groups = shippingGroups.enumerated().map { index, group in + let option = """ + { + "handle":"shipping-\(index)", + "title":"Shipping \(index)", + "deliveryMethodType":"SHIPPING", + "estimatedCost":{"amount":"\(group.amount)","currencyCode":"USD"} + } + """ + return """ + { + "id":"gid://shopify/CartDeliveryGroup/\(index)", + "groupType":"\(group.subscription ? "SUBSCRIPTION" : "ONE_TIME_PURCHASE")", + "deliveryOptions":[\(option)], + "selectedDeliveryOption":\(option) + } + """ + } + let json = """ + { + "id":"gid://shopify/Cart/discount-test", + "checkoutUrl":"https://test-shop.myshopify.com/checkout", + "totalQuantity":\(lineSubtotals.count * 2), + "deliveryGroups":{"nodes":[\(groups.joined(separator: ","))]}, + "lines":{"nodes":[\(lines.joined(separator: ","))]}, + "cost":{"totalAmount":{"amount":"\(totalAmount)","currencyCode":"USD"}}, + "discountApplications":\(applications) + } + """ + return try JSONDecoder().decode(StorefrontAPI.Cart.self, from: Data(json.utf8)) + } } From 49fe61bd4f1b09fd79c26a48826d8cf29ac17cb6 Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Thu, 17 Sep 2026 17:22:22 +0100 Subject: [PATCH 3/5] Revert "Migrate accelerated checkout discounts to cart applications" This reverts commit 0b6da87ae535a708d6b1977c22ade5952c694131. --- .../GraphQLDocument+Fragments.swift | 140 +++++++++++- .../StorefrontAPI/StorefrontAPI+Types.swift | 131 ++++++++++- .../ApplePay/Data/PassKitFactory.swift | 103 ++++++++- .../StorefrontAPIMutationsTests.swift | 45 ++-- .../StorefrontAPIQueriesTests.swift | 55 +---- .../TestHelpers.swift | 9 +- ...AuthorizationDelegateControllerTests.swift | 4 +- .../ApplePayAuthorizationDelegateTests.swift | 3 +- .../ErrorHandler/ErrorHandlerTests.swift | 3 +- .../ErrorHandler_WarningsTests.swift | 6 +- .../Wallets/ApplePay/PKDecoderTests.swift | 3 +- .../ApplePay/PassKitFactoryTests.swift | 205 ------------------ 12 files changed, 420 insertions(+), 287 deletions(-) diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift index fe150e79a..79e17de61 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift @@ -60,14 +60,75 @@ extension GraphQLDocument { currencyCode } } - discountApplications { - targetType - totalAllocatedAmount { - amount - currencyCode + discountCodes { + applicable + code + } + discountAllocations { + __typename + ... on CartAutomaticDiscountAllocation { + discountedAmount { + amount + currencyCode + } + targetType + discountApplication { + targetSelection + targetType + value { + __typename + ... on MoneyV2 { + amount + currencyCode + } + ... on PricingPercentageValue { + percentage + } + } + } } - ... on CartCodeDiscountApplication { + ... on CartCodeDiscountAllocation { code + discountedAmount { + amount + currencyCode + } + targetType + discountApplication { + targetSelection + targetType + value { + __typename + ... on MoneyV2 { + amount + currencyCode + } + ... on PricingPercentageValue { + percentage + } + } + } + } + ... on CartCustomDiscountAllocation { + discountedAmount { + amount + currencyCode + } + targetType + discountApplication { + targetSelection + targetType + value { + __typename + ... on MoneyV2 { + amount + currencyCode + } + ... on PricingPercentageValue { + percentage + } + } + } } } } @@ -134,6 +195,73 @@ extension GraphQLDocument { currencyCode } } + discountAllocations { + __typename + ... on CartAutomaticDiscountAllocation { + discountedAmount { + amount + currencyCode + } + targetType + discountApplication { + targetSelection + targetType + value { + __typename + ... on MoneyV2 { + amount + currencyCode + } + ... on PricingPercentageValue { + percentage + } + } + } + } + ... on CartCodeDiscountAllocation { + code + discountedAmount { + amount + currencyCode + } + targetType + discountApplication { + targetSelection + targetType + value { + __typename + ... on MoneyV2 { + amount + currencyCode + } + ... on PricingPercentageValue { + percentage + } + } + } + } + ... on CartCustomDiscountAllocation { + discountedAmount { + amount + currencyCode + } + targetType + discountApplication { + targetSelection + targetType + value { + __typename + ... on MoneyV2 { + amount + currencyCode + } + ... on PricingPercentageValue { + percentage + } + } + } + } + } } """ diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift index 7a684184f..b5e95c3cd 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift @@ -27,7 +27,14 @@ extension StorefrontAPI { let delivery: CartDelivery? let lines: BaseCartLineConnection let cost: CartCost - let discountApplications: [CartDiscountApplication] + let discountCodes: [CartDiscountCode] + let discountAllocations: [CartDiscountAllocation] + } + + /// Cart discount code + struct CartDiscountCode: Codable { + let code: String + let applicable: Bool } /// Cart buyer identity @@ -129,11 +136,85 @@ extension StorefrontAPI { let nodes: [BaseCartLine] } + /// Cart discount allocation + enum CartDiscountAllocation: Codable { + case automatic(CartAutomaticDiscountAllocation) + case code(CartCodeDiscountAllocation) + case custom(CartCustomDiscountAllocation) + + private enum CodingKeys: String, CodingKey { + case __typename + } + + private enum TypeName: String, Codable { + case cartAutomaticDiscountAllocation = "CartAutomaticDiscountAllocation" + case cartCodeDiscountAllocation = "CartCodeDiscountAllocation" + case cartCustomDiscountAllocation = "CartCustomDiscountAllocation" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let typename = try container.decode(TypeName.self, forKey: .__typename) + + switch typename { + case .cartAutomaticDiscountAllocation: + let allocation = try CartAutomaticDiscountAllocation(from: decoder) + self = .automatic(allocation) + case .cartCodeDiscountAllocation: + let allocation = try CartCodeDiscountAllocation(from: decoder) + self = .code(allocation) + case .cartCustomDiscountAllocation: + let allocation = try CartCustomDiscountAllocation(from: decoder) + self = .custom(allocation) + } + } + + func encode(to encoder: Encoder) throws { + switch self { + case let .automatic(allocation): + try allocation.encode(to: encoder) + case let .code(allocation): + try allocation.encode(to: encoder) + case let .custom(allocation): + try allocation.encode(to: encoder) + } + } + } + + /// Automatic discount allocation + struct CartAutomaticDiscountAllocation: Codable { + let discountApplication: CartDiscountApplication + let discountedAmount: MoneyV2 + let targetType: DiscountApplicationTargetType + } + + /// Code discount allocation + struct CartCodeDiscountAllocation: Codable { + let code: String + let discountApplication: CartDiscountApplication + let discountedAmount: MoneyV2 + let targetType: DiscountApplicationTargetType + } + + /// Custom discount allocation + struct CartCustomDiscountAllocation: Codable { + let discountApplication: CartDiscountApplication + let discountedAmount: MoneyV2 + let targetType: DiscountApplicationTargetType + } + /// Cart discount application struct CartDiscountApplication: Codable { + let targetSelection: DiscountApplicationTargetSelection let targetType: DiscountApplicationTargetType - let totalAllocatedAmount: MoneyV2 - let code: String? + let value: PricingValue + } + + /// Discount application target selection + enum DiscountApplicationTargetSelection: String, Codable { + case all = "ALL" + case entitled = "ENTITLED" + case explicit = "EXPLICIT" } /// Discount application target type @@ -142,12 +223,56 @@ extension StorefrontAPI { case shippingLine = "SHIPPING_LINE" } + /// Pricing value (union type for percentage or fixed amount) + enum PricingValue: Codable { + case percentage(PricingPercentageValue) + case fixedAmount(MoneyV2) + + private enum CodingKeys: String, CodingKey { + case __typename + } + + private enum TypeName: String, Codable { + case pricingPercentageValue = "PricingPercentageValue" + case moneyV2 = "MoneyV2" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let typename = try container.decode(TypeName.self, forKey: .__typename) + + switch typename { + case .pricingPercentageValue: + let percentage = try PricingPercentageValue(from: decoder) + self = .percentage(percentage) + case .moneyV2: + let amount = try MoneyV2(from: decoder) + self = .fixedAmount(amount) + } + } + + func encode(to encoder: Encoder) throws { + switch self { + case let .percentage(percentage): + try percentage.encode(to: encoder) + case let .fixedAmount(amount): + try amount.encode(to: encoder) + } + } + } + + /// Pricing percentage value + struct PricingPercentageValue: Codable { + let percentage: Double + } + /// Base cart line struct BaseCartLine: Codable { let id: Types.ID let quantity: Int let merchandise: ProductVariant? let cost: CartLineCost + let discountAllocations: [CartDiscountAllocation] } /// Cart line cost diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift index e91ff7027..137de8f6e 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift @@ -103,7 +103,8 @@ final class PassKitFactory: Sendable { } } - /// Gets the allocated totals from cart discount applications for display. + /// Gets all discount allocations for line items, including applicable discount codes + /// that are not already accounted for in cart or line item allocations. /// /// - Parameter cart: The cart to get discount allocations from /// - Returns: An array of DiscountAllocationInfo representing all discounts @@ -115,15 +116,107 @@ final class PassKitFactory: Sendable { throw ShopifyAcceleratedCheckouts.Error.invariant(expected: "cart") } - return cart.discountApplications.map { application in + let currencyCode = cart.cost.totalAmount.currencyCode + + // Get all discount allocations from line items + let lineItemDiscountAllocations = cart.lines.nodes.flatMap { $0.discountAllocations } + + // Find discount codes that are applicable but not already accounted for + let applicableOtherDiscountCodes = findApplicableUnaccountedDiscountCodes( + cart: cart, + lineItemDiscountAllocations: lineItemDiscountAllocations + ) + + // Map applicable discount codes to synthetic discount allocations with 0 amount + // These are typically shipping discounts that don't show up in other allocations + let shippingDiscounts = createShippingDiscounts( + discountCodes: applicableOtherDiscountCodes, + currencyCode: currencyCode + ) + + // Convert cart discount allocations + let cartDiscounts = convertDiscountAllocations(cart.discountAllocations) + + // Convert line item discount allocations + let productDiscounts = convertDiscountAllocations(lineItemDiscountAllocations) + + // Return combined array: shipping discounts + cart discounts + product discounts + return shippingDiscounts + cartDiscounts + productDiscounts + } + + /// Finds discount codes that are applicable but not already accounted for in allocations + private func findApplicableUnaccountedDiscountCodes( + cart: StorefrontAPI.Cart, + lineItemDiscountAllocations: [StorefrontAPI.CartDiscountAllocation] + ) -> [StorefrontAPI.CartDiscountCode] { + return cart.discountCodes.filter { discountCode in + guard discountCode.applicable else { return false } + + // Check if not in cart discount allocations + let allocations = + (lineItemDiscountAllocations + cart.discountAllocations) + .contains { allocation in + if case let .code(codeAllocation) = allocation { + return codeAllocation.code == discountCode.code + } + return false + } + + return !allocations + } + } + + /// Creates shipping discount allocations with 0 amount for applicable discount codes + private func createShippingDiscounts( + discountCodes: [StorefrontAPI.CartDiscountCode], + currencyCode: String + ) -> [DiscountAllocationInfo] { + return discountCodes.map { discountCode in DiscountAllocationInfo( - code: application.code, - amount: application.totalAllocatedAmount.amount, - currencyCode: application.totalAllocatedAmount.currencyCode + code: discountCode.code, + amount: 0, + currencyCode: currencyCode ) } } + /// Converts cart discount allocations to DiscountAllocationInfo + private func convertDiscountAllocations( + _ allocations: [StorefrontAPI.CartDiscountAllocation] + ) -> [DiscountAllocationInfo] { + return allocations.compactMap { allocation in + convertSingleDiscountAllocation(allocation) + } + } + + /// Converts a single discount allocation to DiscountAllocationInfo + private func convertSingleDiscountAllocation( + _ allocation: StorefrontAPI.CartDiscountAllocation + ) -> DiscountAllocationInfo { + let code: String? = + if case let .code(codeAllocation) = allocation { + codeAllocation.code + } else { + nil + } + + let (discountedAmount, currencyCode) = + switch allocation { + case let .automatic(auto): + (auto.discountedAmount.amount, auto.discountedAmount.currencyCode) + case let .code(code): + (code.discountedAmount.amount, code.discountedAmount.currencyCode) + case let .custom(custom): + (custom.discountedAmount.amount, custom.discountedAmount.currencyCode) + } + + return DiscountAllocationInfo( + code: code, + amount: discountedAmount, + currencyCode: currencyCode + ) + } + /// Computes the cartesian product of a 2D array. The cartesian product is the set of all possible /// ordered combinations of elements from the input arrays. /// diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift index 408a18f7c..1383ed0c9 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift @@ -113,7 +113,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "cost": { "totalAmount": {"amount": "19.99", "currencyCode": "USD"}, "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"} - } + }, + "discountAllocations": [] }] }, "cost": { @@ -121,7 +122,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -155,7 +157,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "0.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -214,7 +217,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": null, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -268,7 +272,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -329,7 +334,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -394,7 +400,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "29.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -473,7 +480,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -646,7 +654,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -686,7 +695,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": {"amount": "2.50", "currencyCode": "USD"} }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -819,7 +829,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": {"amount": "3.00", "currencyCode": "USD"} }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -888,7 +899,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [] } @@ -924,7 +936,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": {"amount": "2.50", "currencyCode": "USD"} }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "checkoutURL": "https://test.myshopify.com/checkout/ready-123" }, @@ -1306,7 +1319,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "0.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [{ "field": ["input"], @@ -1350,7 +1364,8 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "0.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] }, "userErrors": [{ "field": ["buyerIdentity", "email"], diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift index dd4891743..94be7c0c9 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift @@ -84,7 +84,8 @@ final class StorefrontAPIQueriesTests: XCTestCase { "cost": { "totalAmount": {"amount": "39.98", "currencyCode": "USD"}, "subtotalAmount": {"amount": "39.98", "currencyCode": "USD"} - } + }, + "discountAllocations": [] }] }, "cost": { @@ -92,7 +93,8 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": {"amount": "39.98", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] } } } @@ -143,7 +145,8 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": null, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] } } } @@ -276,7 +279,8 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": null, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] } }, "errors": [ @@ -373,7 +377,8 @@ final class StorefrontAPIQueriesTests: XCTestCase { "cost": { "totalAmount": {"amount": "10.00", "currencyCode": "USD"}, "subtotalAmount": {"amount": "10.00", "currencyCode": "USD"} - } + }, + "discountAllocations": [] } """) } @@ -396,7 +401,8 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": {"amount": "1000.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountApplications": [] + "discountCodes": [], + "discountAllocations": [] } } } @@ -441,41 +447,4 @@ final class StorefrontAPIQueriesTests: XCTestCase { XCTFail("Failed to parse request body: \(error)") } } - - func testCartQueryDecodesDiscountApplicationsWithoutLegacyAllocations() async throws { - mockJSONResponse(""" - {"data":{"cart":{ - "id":"gid://shopify/Cart/discounts", - "checkoutUrl":"https://test.myshopify.com/checkout/discounts", - "totalQuantity":0, - "deliveryGroups":{"nodes":[]}, - "lines":{"nodes":[]}, - "cost":{"totalAmount":{"amount":"0.00","currencyCode":"USD"}}, - "discountApplications":[ - {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"5.25","currencyCode":"USD"},"code":"SAVE"}, - {"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"3.00","currencyCode":"USD"}} - ] - }}} - """) - - let cart = try await storefrontAPI.cart(by: GraphQLScalars.ID("gid://shopify/Cart/discounts")) - let discounts = try XCTUnwrap(cart?.discountApplications) - - XCTAssertEqual(discounts.count, 2) - XCTAssertEqual(discounts.first?.code, "SAVE") - XCTAssertEqual(discounts.first?.totalAllocatedAmount.amount, Decimal(string: "5.25")) - XCTAssertEqual(discounts.first?.targetType, .lineItem) - XCTAssertNil(discounts.last?.code) - XCTAssertEqual(discounts.last?.targetType, .shippingLine) - - let requestBody = try XCTUnwrap(MockURLProtocol.capturedRequestBody) - let body = try XCTUnwrap(JSONSerialization.jsonObject(with: requestBody) as? [String: Any]) - let query = try XCTUnwrap(body["query"] as? String) - - XCTAssertTrue(query.contains("discountApplications {")) - XCTAssertTrue(query.contains("totalAllocatedAmount {")) - XCTAssertTrue(query.contains("... on CartCodeDiscountApplication")) - XCTAssertFalse(query.contains("discountAllocations")) - XCTAssertFalse(query.contains("sourceDiscountApplication")) - } } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift index 7241bcdb1..e33406614 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift @@ -184,7 +184,8 @@ extension StorefrontAPI.Cart { totalTaxAmount: nil, totalDutyAmount: nil ), - discountApplications: [] + discountCodes: [], + discountAllocations: [] ) } @@ -212,7 +213,8 @@ extension StorefrontAPI.Cart { totalTaxAmount: nil, totalDutyAmount: nil ), - discountApplications: [] + discountCodes: [], + discountAllocations: [] ) } @@ -263,7 +265,8 @@ extension StorefrontAPI.Cart { totalTaxAmount: nil, totalDutyAmount: nil ), - discountApplications: [] + discountCodes: [], + discountAllocations: [] ) } } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift index 3794695c6..c7ed15aa2 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift @@ -587,7 +587,7 @@ final class ApplePayAuthorizationDelegateControllerTests: XCTestCase { "\"delivery\":null," + "\"lines\":{\"nodes\":[]}," + "\"cost\":{\"totalAmount\":{\"amount\":\"0.00\",\"currencyCode\":\"USD\"}}," + - "\"discountApplications\":[]}" + "\"discountCodes\":[],\"discountAllocations\":[]}" static let mockCartWithAddressResponse: String = "{" + "\"id\":\"gid://shopify/Cart/test\"," + @@ -610,7 +610,7 @@ final class ApplePayAuthorizationDelegateControllerTests: XCTestCase { "\"delivery\":{\"addresses\":[{\"id\":\"gid://shopify/CartSelectableAddress/1\",\"selected\":true,\"address\":{\"countryCode\":\"US\"}}]}," + "\"lines\":{\"nodes\":[]}," + "\"cost\":{\"totalAmount\":{\"amount\":\"0.00\",\"currencyCode\":\"USD\"}}," + - "\"discountApplications\":[]}" + "\"discountCodes\":[],\"discountAllocations\":[]}" private struct State { var failReplace = false diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift index f09bb4689..e9d5c312e 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift @@ -639,7 +639,8 @@ final class ApplePayAuthorizationDelegateTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountApplications: [] + discountCodes: [], + discountAllocations: [] ) } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift index 6aec7c8cd..2ae6986fe 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift @@ -207,7 +207,8 @@ class ErrorHandlerTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountApplications: [] + discountCodes: [], + discountAllocations: [] ) } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift index 8ffa1226d..914c02dfc 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift @@ -19,7 +19,8 @@ class ErrorHandler_WarningsTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountApplications: [] + discountCodes: [], + discountAllocations: [] ) let result = ErrorHandler.map(warningType: StorefrontAPI.WarningType.outOfStock, cart: cart) @@ -49,7 +50,8 @@ class ErrorHandler_WarningsTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountApplications: [] + discountCodes: [], + discountAllocations: [] ) let result = ErrorHandler.map(warningType: StorefrontAPI.WarningType.notEnoughStock, cart: cart) diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift index 7f6f0125a..54244897f 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift @@ -38,7 +38,8 @@ class PKDecoderTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountApplications: [] + discountCodes: [], + discountAllocations: [] ) } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift index cbfe01325..95f0ae1e6 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift @@ -108,209 +108,4 @@ class PassKitFactoryTests: XCTestCase { XCTAssertEqual(secondCombination?.amount, NSDecimalNumber(decimal: 25.99)) XCTAssertEqual(secondCombination?.detail, "1 to 2 days and 5 to 7 days") } - - func testFixedDiscountAcrossMultipleLinesUsesAllocatedTotalOnce() throws { - let cart = try makeDiscountCart( - applications: """ - [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"7.00","currencyCode":"USD"},"code":"SAVE7"}] - """, - lineSubtotals: ["30.00", "70.00"], - totalAmount: "93.00" - ) - - let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") - - XCTAssertEqual(items.first?.amount, NSDecimalNumber(string: "100.00")) - XCTAssertEqual(items.filter { $0.label == "SAVE7" }.count, 1) - XCTAssertEqual(items.first { $0.label == "SAVE7" }?.amount, NSDecimalNumber(string: "-7.00")) - XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "93.00")) - XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 93) - } - - func testPercentageDiscountUsesAllocatedMoneyAmount() throws { - let cart = try makeDiscountCart( - applications: """ - [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"17.49","currencyCode":"USD"},"code":"PERCENT15"}] - """, - lineSubtotals: ["116.60"], - totalAmount: "99.11" - ) - - let discounts = try PassKitFactory.shared.createDiscountAllocations(cart: cart) - let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") - - XCTAssertEqual(discounts.count, 1) - XCTAssertEqual(discounts.first?.amount, Decimal(string: "17.49")) - XCTAssertEqual(discounts.first?.currencyCode, "USD") - XCTAssertEqual(items.first { $0.label == "PERCENT15" }?.amount, NSDecimalNumber(string: "-17.49")) - XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "99.11")) - } - - func testAutomaticAndCustomDiscountsShareGeneralLabelAndPreserveCodeLabels() throws { - let cart = try makeDiscountCart( - applications: """ - [ - {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"4.00","currencyCode":"USD"}}, - {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"1.50","currencyCode":"USD"}}, - {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"3.00","currencyCode":"USD"},"code":"SAVE"}, - {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"2.00","currencyCode":"USD"},"code":"SAVE"} - ] - """, - totalAmount: "89.50" - ) - - let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") - - XCTAssertEqual(items.first { $0.label == "order_summary.discount".localizedString }?.amount, NSDecimalNumber(string: "-5.50")) - XCTAssertEqual(items.first { $0.label == "SAVE" }?.amount, NSDecimalNumber(string: "-5.00")) - XCTAssertEqual(items.filter { $0.amount.decimalValue < 0 }.count, 2) - XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "89.50")) - } - - func testZeroAllocationDoesNotAddDiscountSummaryRow() throws { - let cart = try makeDiscountCart( - applications: """ - [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"0.00","currencyCode":"USD"},"code":"ZERO"}] - """, - totalAmount: "100.00" - ) - - let discounts = try PassKitFactory.shared.createDiscountAllocations(cart: cart) - let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") - - XCTAssertEqual(discounts.first?.amount, 0) - XCTAssertFalse(items.contains { $0.label == "ZERO" }) - XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "100.00")) - } - - func testEmptyApplicationsDoNotAddDiscounts() throws { - let cart = try makeDiscountCart( - applications: "[]", - totalAmount: "100.00" - ) - - XCTAssertTrue(try PassKitFactory.shared.createDiscountAllocations(cart: cart).isEmpty) - } - - func testSummaryUsesAuthoritativeCartTotal() throws { - let cart = try makeDiscountCart( - applications: """ - [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"10.00","currencyCode":"USD"},"code":"SAVE10"}] - """, - totalAmount: "95.25" - ) - - let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") - - XCTAssertEqual(items.last?.label, "Test Shop") - XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "95.25")) - } - - func testFreeShippingDeductsShippingApplicationOnce() throws { - let cart = try makeDiscountCart( - applications: """ - [{"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"10.00","currencyCode":"USD"},"code":"FREESHIP"}] - """, - shippingGroups: [("10.00", false)], - totalAmount: "100.00" - ) - - let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") - - XCTAssertEqual(items.first { $0.label == "order_summary.shipping".localizedString }?.amount, NSDecimalNumber(string: "10.00")) - XCTAssertEqual(items.first { $0.label == "FREESHIP" }?.amount, NSDecimalNumber(string: "-10.00")) - XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 100) - XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "100.00")) - } - - func testPartialShippingDiscountPreservesEstimatedShippingCost() throws { - let cart = try makeDiscountCart( - applications: """ - [{"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"4.00","currencyCode":"USD"},"code":"SHIP4"}] - """, - shippingGroups: [("10.00", false)], - totalAmount: "106.00" - ) - - let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") - let shippingMethods = PassKitFactory.shared.createShippingMethods(deliveryGroups: cart.deliveryGroups.nodes) - - XCTAssertEqual(items.first { $0.label == "order_summary.shipping".localizedString }?.amount, NSDecimalNumber(string: "10.00")) - XCTAssertEqual(items.first { $0.label == "SHIP4" }?.amount, NSDecimalNumber(string: "-4.00")) - XCTAssertEqual(shippingMethods.first?.amount, NSDecimalNumber(string: "10.00")) - XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 106) - } - - func testCombinedProductAndShippingCodeAcrossDeliveryGroupsCountsEachApplicationOnce() throws { - let cart = try makeDiscountCart( - applications: """ - [ - {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"5.00","currencyCode":"USD"},"code":"COMBINED"}, - {"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"4.00","currencyCode":"USD"},"code":"COMBINED"} - ] - """, - shippingGroups: [("8.00", false), ("2.00", true)], - totalAmount: "101.00" - ) - - let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") - - XCTAssertEqual(items.first { $0.label == "order_summary.shipping_one_time_purchase".localizedString }?.amount, NSDecimalNumber(string: "8.00")) - XCTAssertEqual(items.first { $0.label == "order_summary.shipping_subscription".localizedString }?.amount, NSDecimalNumber(string: "2.00")) - XCTAssertEqual(items.filter { $0.label == "COMBINED" }.count, 1) - XCTAssertEqual(items.first { $0.label == "COMBINED" }?.amount, NSDecimalNumber(string: "-9.00")) - XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 101) - XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "101.00")) - } - - private func makeDiscountCart( - applications: String, - lineSubtotals: [String] = ["100.00"], - shippingGroups: [(amount: String, subscription: Bool)] = [], - totalAmount: String - ) throws -> StorefrontAPI.Cart { - let lines = lineSubtotals.enumerated().map { index, subtotal in - """ - { - "id":"gid://shopify/CartLine/\(index)", - "quantity":2, - "merchandise":null, - "cost":{ - "totalAmount":{"amount":"\(subtotal)","currencyCode":"USD"}, - "subtotalAmount":{"amount":"\(subtotal)","currencyCode":"USD"} - } - } - """ - } - let groups = shippingGroups.enumerated().map { index, group in - let option = """ - { - "handle":"shipping-\(index)", - "title":"Shipping \(index)", - "deliveryMethodType":"SHIPPING", - "estimatedCost":{"amount":"\(group.amount)","currencyCode":"USD"} - } - """ - return """ - { - "id":"gid://shopify/CartDeliveryGroup/\(index)", - "groupType":"\(group.subscription ? "SUBSCRIPTION" : "ONE_TIME_PURCHASE")", - "deliveryOptions":[\(option)], - "selectedDeliveryOption":\(option) - } - """ - } - let json = """ - { - "id":"gid://shopify/Cart/discount-test", - "checkoutUrl":"https://test-shop.myshopify.com/checkout", - "totalQuantity":\(lineSubtotals.count * 2), - "deliveryGroups":{"nodes":[\(groups.joined(separator: ","))]}, - "lines":{"nodes":[\(lines.joined(separator: ","))]}, - "cost":{"totalAmount":{"amount":"\(totalAmount)","currencyCode":"USD"}}, - "discountApplications":\(applications) - } - """ - return try JSONDecoder().decode(StorefrontAPI.Cart.self, from: Data(json.utf8)) - } } From c33c0e359f0d6513217306c6e1d962668f4fbef3 Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Wed, 16 Sep 2026 16:32:55 +0100 Subject: [PATCH 4/5] Migrate accelerated checkout discounts to cart applications (cherry picked from commit 0b6da87ae535a708d6b1977c22ade5952c694131) --- .../GraphQLDocument+Fragments.swift | 140 +----------- .../StorefrontAPI/StorefrontAPI+Types.swift | 131 +---------- .../ApplePay/Data/PassKitFactory.swift | 103 +-------- .../StorefrontAPIMutationsTests.swift | 45 ++-- .../StorefrontAPIQueriesTests.swift | 55 ++++- .../TestHelpers.swift | 9 +- ...AuthorizationDelegateControllerTests.swift | 4 +- .../ApplePayAuthorizationDelegateTests.swift | 3 +- .../ErrorHandler/ErrorHandlerTests.swift | 3 +- .../ErrorHandler_WarningsTests.swift | 6 +- .../Wallets/ApplePay/PKDecoderTests.swift | 3 +- .../ApplePay/PassKitFactoryTests.swift | 205 ++++++++++++++++++ 12 files changed, 287 insertions(+), 420 deletions(-) diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift index 79e17de61..fe150e79a 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLDocument/GraphQLDocument+Fragments.swift @@ -60,75 +60,14 @@ extension GraphQLDocument { currencyCode } } - discountCodes { - applicable - code - } - discountAllocations { - __typename - ... on CartAutomaticDiscountAllocation { - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } + discountApplications { + targetType + totalAllocatedAmount { + amount + currencyCode } - ... on CartCodeDiscountAllocation { + ... on CartCodeDiscountApplication { code - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } - } - ... on CartCustomDiscountAllocation { - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } } } } @@ -195,73 +134,6 @@ extension GraphQLDocument { currencyCode } } - discountAllocations { - __typename - ... on CartAutomaticDiscountAllocation { - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } - } - ... on CartCodeDiscountAllocation { - code - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } - } - ... on CartCustomDiscountAllocation { - discountedAmount { - amount - currencyCode - } - targetType - discountApplication { - targetSelection - targetType - value { - __typename - ... on MoneyV2 { - amount - currencyCode - } - ... on PricingPercentageValue { - percentage - } - } - } - } - } } """ diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift index b5e95c3cd..7a684184f 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Types.swift @@ -27,14 +27,7 @@ extension StorefrontAPI { let delivery: CartDelivery? let lines: BaseCartLineConnection let cost: CartCost - let discountCodes: [CartDiscountCode] - let discountAllocations: [CartDiscountAllocation] - } - - /// Cart discount code - struct CartDiscountCode: Codable { - let code: String - let applicable: Bool + let discountApplications: [CartDiscountApplication] } /// Cart buyer identity @@ -136,85 +129,11 @@ extension StorefrontAPI { let nodes: [BaseCartLine] } - /// Cart discount allocation - enum CartDiscountAllocation: Codable { - case automatic(CartAutomaticDiscountAllocation) - case code(CartCodeDiscountAllocation) - case custom(CartCustomDiscountAllocation) - - private enum CodingKeys: String, CodingKey { - case __typename - } - - private enum TypeName: String, Codable { - case cartAutomaticDiscountAllocation = "CartAutomaticDiscountAllocation" - case cartCodeDiscountAllocation = "CartCodeDiscountAllocation" - case cartCustomDiscountAllocation = "CartCustomDiscountAllocation" - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let typename = try container.decode(TypeName.self, forKey: .__typename) - - switch typename { - case .cartAutomaticDiscountAllocation: - let allocation = try CartAutomaticDiscountAllocation(from: decoder) - self = .automatic(allocation) - case .cartCodeDiscountAllocation: - let allocation = try CartCodeDiscountAllocation(from: decoder) - self = .code(allocation) - case .cartCustomDiscountAllocation: - let allocation = try CartCustomDiscountAllocation(from: decoder) - self = .custom(allocation) - } - } - - func encode(to encoder: Encoder) throws { - switch self { - case let .automatic(allocation): - try allocation.encode(to: encoder) - case let .code(allocation): - try allocation.encode(to: encoder) - case let .custom(allocation): - try allocation.encode(to: encoder) - } - } - } - - /// Automatic discount allocation - struct CartAutomaticDiscountAllocation: Codable { - let discountApplication: CartDiscountApplication - let discountedAmount: MoneyV2 - let targetType: DiscountApplicationTargetType - } - - /// Code discount allocation - struct CartCodeDiscountAllocation: Codable { - let code: String - let discountApplication: CartDiscountApplication - let discountedAmount: MoneyV2 - let targetType: DiscountApplicationTargetType - } - - /// Custom discount allocation - struct CartCustomDiscountAllocation: Codable { - let discountApplication: CartDiscountApplication - let discountedAmount: MoneyV2 - let targetType: DiscountApplicationTargetType - } - /// Cart discount application struct CartDiscountApplication: Codable { - let targetSelection: DiscountApplicationTargetSelection let targetType: DiscountApplicationTargetType - let value: PricingValue - } - - /// Discount application target selection - enum DiscountApplicationTargetSelection: String, Codable { - case all = "ALL" - case entitled = "ENTITLED" - case explicit = "EXPLICIT" + let totalAllocatedAmount: MoneyV2 + let code: String? } /// Discount application target type @@ -223,56 +142,12 @@ extension StorefrontAPI { case shippingLine = "SHIPPING_LINE" } - /// Pricing value (union type for percentage or fixed amount) - enum PricingValue: Codable { - case percentage(PricingPercentageValue) - case fixedAmount(MoneyV2) - - private enum CodingKeys: String, CodingKey { - case __typename - } - - private enum TypeName: String, Codable { - case pricingPercentageValue = "PricingPercentageValue" - case moneyV2 = "MoneyV2" - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let typename = try container.decode(TypeName.self, forKey: .__typename) - - switch typename { - case .pricingPercentageValue: - let percentage = try PricingPercentageValue(from: decoder) - self = .percentage(percentage) - case .moneyV2: - let amount = try MoneyV2(from: decoder) - self = .fixedAmount(amount) - } - } - - func encode(to encoder: Encoder) throws { - switch self { - case let .percentage(percentage): - try percentage.encode(to: encoder) - case let .fixedAmount(amount): - try amount.encode(to: encoder) - } - } - } - - /// Pricing percentage value - struct PricingPercentageValue: Codable { - let percentage: Double - } - /// Base cart line struct BaseCartLine: Codable { let id: Types.ID let quantity: Int let merchandise: ProductVariant? let cost: CartLineCost - let discountAllocations: [CartDiscountAllocation] } /// Cart line cost diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift index 137de8f6e..e91ff7027 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/Data/PassKitFactory.swift @@ -103,8 +103,7 @@ final class PassKitFactory: Sendable { } } - /// Gets all discount allocations for line items, including applicable discount codes - /// that are not already accounted for in cart or line item allocations. + /// Gets the allocated totals from cart discount applications for display. /// /// - Parameter cart: The cart to get discount allocations from /// - Returns: An array of DiscountAllocationInfo representing all discounts @@ -116,107 +115,15 @@ final class PassKitFactory: Sendable { throw ShopifyAcceleratedCheckouts.Error.invariant(expected: "cart") } - let currencyCode = cart.cost.totalAmount.currencyCode - - // Get all discount allocations from line items - let lineItemDiscountAllocations = cart.lines.nodes.flatMap { $0.discountAllocations } - - // Find discount codes that are applicable but not already accounted for - let applicableOtherDiscountCodes = findApplicableUnaccountedDiscountCodes( - cart: cart, - lineItemDiscountAllocations: lineItemDiscountAllocations - ) - - // Map applicable discount codes to synthetic discount allocations with 0 amount - // These are typically shipping discounts that don't show up in other allocations - let shippingDiscounts = createShippingDiscounts( - discountCodes: applicableOtherDiscountCodes, - currencyCode: currencyCode - ) - - // Convert cart discount allocations - let cartDiscounts = convertDiscountAllocations(cart.discountAllocations) - - // Convert line item discount allocations - let productDiscounts = convertDiscountAllocations(lineItemDiscountAllocations) - - // Return combined array: shipping discounts + cart discounts + product discounts - return shippingDiscounts + cartDiscounts + productDiscounts - } - - /// Finds discount codes that are applicable but not already accounted for in allocations - private func findApplicableUnaccountedDiscountCodes( - cart: StorefrontAPI.Cart, - lineItemDiscountAllocations: [StorefrontAPI.CartDiscountAllocation] - ) -> [StorefrontAPI.CartDiscountCode] { - return cart.discountCodes.filter { discountCode in - guard discountCode.applicable else { return false } - - // Check if not in cart discount allocations - let allocations = - (lineItemDiscountAllocations + cart.discountAllocations) - .contains { allocation in - if case let .code(codeAllocation) = allocation { - return codeAllocation.code == discountCode.code - } - return false - } - - return !allocations - } - } - - /// Creates shipping discount allocations with 0 amount for applicable discount codes - private func createShippingDiscounts( - discountCodes: [StorefrontAPI.CartDiscountCode], - currencyCode: String - ) -> [DiscountAllocationInfo] { - return discountCodes.map { discountCode in + return cart.discountApplications.map { application in DiscountAllocationInfo( - code: discountCode.code, - amount: 0, - currencyCode: currencyCode + code: application.code, + amount: application.totalAllocatedAmount.amount, + currencyCode: application.totalAllocatedAmount.currencyCode ) } } - /// Converts cart discount allocations to DiscountAllocationInfo - private func convertDiscountAllocations( - _ allocations: [StorefrontAPI.CartDiscountAllocation] - ) -> [DiscountAllocationInfo] { - return allocations.compactMap { allocation in - convertSingleDiscountAllocation(allocation) - } - } - - /// Converts a single discount allocation to DiscountAllocationInfo - private func convertSingleDiscountAllocation( - _ allocation: StorefrontAPI.CartDiscountAllocation - ) -> DiscountAllocationInfo { - let code: String? = - if case let .code(codeAllocation) = allocation { - codeAllocation.code - } else { - nil - } - - let (discountedAmount, currencyCode) = - switch allocation { - case let .automatic(auto): - (auto.discountedAmount.amount, auto.discountedAmount.currencyCode) - case let .code(code): - (code.discountedAmount.amount, code.discountedAmount.currencyCode) - case let .custom(custom): - (custom.discountedAmount.amount, custom.discountedAmount.currencyCode) - } - - return DiscountAllocationInfo( - code: code, - amount: discountedAmount, - currencyCode: currencyCode - ) - } - /// Computes the cartesian product of a 2D array. The cartesian product is the set of all possible /// ordered combinations of elements from the input arrays. /// diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift index 1383ed0c9..408a18f7c 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIMutationsTests.swift @@ -113,8 +113,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "cost": { "totalAmount": {"amount": "19.99", "currencyCode": "USD"}, "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"} - }, - "discountAllocations": [] + } }] }, "cost": { @@ -122,8 +121,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -157,8 +155,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "0.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -217,8 +214,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": null, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -272,8 +268,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -334,8 +329,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -400,8 +394,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "29.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -480,8 +473,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -654,8 +646,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -695,8 +686,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": {"amount": "2.50", "currencyCode": "USD"} }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -829,8 +819,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": {"amount": "3.00", "currencyCode": "USD"} }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -899,8 +888,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [] } @@ -936,8 +924,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "19.99", "currencyCode": "USD"}, "totalTaxAmount": {"amount": "2.50", "currencyCode": "USD"} }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "checkoutURL": "https://test.myshopify.com/checkout/ready-123" }, @@ -1319,8 +1306,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "0.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [{ "field": ["input"], @@ -1364,8 +1350,7 @@ final class StorefrontAPIMutationsTests: XCTestCase { "subtotalAmount": {"amount": "0.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] }, "userErrors": [{ "field": ["buyerIdentity", "email"], diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift index 94be7c0c9..dd4891743 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/StorefrontAPIQueriesTests.swift @@ -84,8 +84,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "cost": { "totalAmount": {"amount": "39.98", "currencyCode": "USD"}, "subtotalAmount": {"amount": "39.98", "currencyCode": "USD"} - }, - "discountAllocations": [] + } }] }, "cost": { @@ -93,8 +92,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": {"amount": "39.98", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] } } } @@ -145,8 +143,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": null, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] } } } @@ -279,8 +276,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": null, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] } }, "errors": [ @@ -377,8 +373,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "cost": { "totalAmount": {"amount": "10.00", "currencyCode": "USD"}, "subtotalAmount": {"amount": "10.00", "currencyCode": "USD"} - }, - "discountAllocations": [] + } } """) } @@ -401,8 +396,7 @@ final class StorefrontAPIQueriesTests: XCTestCase { "subtotalAmount": {"amount": "1000.00", "currencyCode": "USD"}, "totalTaxAmount": null }, - "discountCodes": [], - "discountAllocations": [] + "discountApplications": [] } } } @@ -447,4 +441,41 @@ final class StorefrontAPIQueriesTests: XCTestCase { XCTFail("Failed to parse request body: \(error)") } } + + func testCartQueryDecodesDiscountApplicationsWithoutLegacyAllocations() async throws { + mockJSONResponse(""" + {"data":{"cart":{ + "id":"gid://shopify/Cart/discounts", + "checkoutUrl":"https://test.myshopify.com/checkout/discounts", + "totalQuantity":0, + "deliveryGroups":{"nodes":[]}, + "lines":{"nodes":[]}, + "cost":{"totalAmount":{"amount":"0.00","currencyCode":"USD"}}, + "discountApplications":[ + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"5.25","currencyCode":"USD"},"code":"SAVE"}, + {"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"3.00","currencyCode":"USD"}} + ] + }}} + """) + + let cart = try await storefrontAPI.cart(by: GraphQLScalars.ID("gid://shopify/Cart/discounts")) + let discounts = try XCTUnwrap(cart?.discountApplications) + + XCTAssertEqual(discounts.count, 2) + XCTAssertEqual(discounts.first?.code, "SAVE") + XCTAssertEqual(discounts.first?.totalAllocatedAmount.amount, Decimal(string: "5.25")) + XCTAssertEqual(discounts.first?.targetType, .lineItem) + XCTAssertNil(discounts.last?.code) + XCTAssertEqual(discounts.last?.targetType, .shippingLine) + + let requestBody = try XCTUnwrap(MockURLProtocol.capturedRequestBody) + let body = try XCTUnwrap(JSONSerialization.jsonObject(with: requestBody) as? [String: Any]) + let query = try XCTUnwrap(body["query"] as? String) + + XCTAssertTrue(query.contains("discountApplications {")) + XCTAssertTrue(query.contains("totalAllocatedAmount {")) + XCTAssertTrue(query.contains("... on CartCodeDiscountApplication")) + XCTAssertFalse(query.contains("discountAllocations")) + XCTAssertFalse(query.contains("sourceDiscountApplication")) + } } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift index e33406614..7241bcdb1 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/TestHelpers.swift @@ -184,8 +184,7 @@ extension StorefrontAPI.Cart { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } @@ -213,8 +212,7 @@ extension StorefrontAPI.Cart { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } @@ -265,8 +263,7 @@ extension StorefrontAPI.Cart { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift index c7ed15aa2..3794695c6 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateControllerTests.swift @@ -587,7 +587,7 @@ final class ApplePayAuthorizationDelegateControllerTests: XCTestCase { "\"delivery\":null," + "\"lines\":{\"nodes\":[]}," + "\"cost\":{\"totalAmount\":{\"amount\":\"0.00\",\"currencyCode\":\"USD\"}}," + - "\"discountCodes\":[],\"discountAllocations\":[]}" + "\"discountApplications\":[]}" static let mockCartWithAddressResponse: String = "{" + "\"id\":\"gid://shopify/Cart/test\"," + @@ -610,7 +610,7 @@ final class ApplePayAuthorizationDelegateControllerTests: XCTestCase { "\"delivery\":{\"addresses\":[{\"id\":\"gid://shopify/CartSelectableAddress/1\",\"selected\":true,\"address\":{\"countryCode\":\"US\"}}]}," + "\"lines\":{\"nodes\":[]}," + "\"cost\":{\"totalAmount\":{\"amount\":\"0.00\",\"currencyCode\":\"USD\"}}," + - "\"discountCodes\":[],\"discountAllocations\":[]}" + "\"discountApplications\":[]}" private struct State { var failReplace = false diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift index e9d5c312e..f09bb4689 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ApplePayAuthorizationDelegate/ApplePayAuthorizationDelegateTests.swift @@ -639,8 +639,7 @@ final class ApplePayAuthorizationDelegateTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift index 2ae6986fe..6aec7c8cd 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandlerTests.swift @@ -207,8 +207,7 @@ class ErrorHandlerTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift index 914c02dfc..8ffa1226d 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/ErrorHandler/ErrorHandler_WarningsTests.swift @@ -19,8 +19,7 @@ class ErrorHandler_WarningsTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) let result = ErrorHandler.map(warningType: StorefrontAPI.WarningType.outOfStock, cart: cart) @@ -50,8 +49,7 @@ class ErrorHandler_WarningsTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) let result = ErrorHandler.map(warningType: StorefrontAPI.WarningType.notEnoughStock, cart: cart) diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift index 54244897f..7f6f0125a 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PKDecoderTests.swift @@ -38,8 +38,7 @@ class PKDecoderTests: XCTestCase { totalTaxAmount: nil, totalDutyAmount: nil ), - discountCodes: [], - discountAllocations: [] + discountApplications: [] ) } diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift index 95f0ae1e6..cbfe01325 100644 --- a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/ApplePay/PassKitFactoryTests.swift @@ -108,4 +108,209 @@ class PassKitFactoryTests: XCTestCase { XCTAssertEqual(secondCombination?.amount, NSDecimalNumber(decimal: 25.99)) XCTAssertEqual(secondCombination?.detail, "1 to 2 days and 5 to 7 days") } + + func testFixedDiscountAcrossMultipleLinesUsesAllocatedTotalOnce() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"7.00","currencyCode":"USD"},"code":"SAVE7"}] + """, + lineSubtotals: ["30.00", "70.00"], + totalAmount: "93.00" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.first?.amount, NSDecimalNumber(string: "100.00")) + XCTAssertEqual(items.filter { $0.label == "SAVE7" }.count, 1) + XCTAssertEqual(items.first { $0.label == "SAVE7" }?.amount, NSDecimalNumber(string: "-7.00")) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "93.00")) + XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 93) + } + + func testPercentageDiscountUsesAllocatedMoneyAmount() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"17.49","currencyCode":"USD"},"code":"PERCENT15"}] + """, + lineSubtotals: ["116.60"], + totalAmount: "99.11" + ) + + let discounts = try PassKitFactory.shared.createDiscountAllocations(cart: cart) + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(discounts.count, 1) + XCTAssertEqual(discounts.first?.amount, Decimal(string: "17.49")) + XCTAssertEqual(discounts.first?.currencyCode, "USD") + XCTAssertEqual(items.first { $0.label == "PERCENT15" }?.amount, NSDecimalNumber(string: "-17.49")) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "99.11")) + } + + func testAutomaticAndCustomDiscountsShareGeneralLabelAndPreserveCodeLabels() throws { + let cart = try makeDiscountCart( + applications: """ + [ + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"4.00","currencyCode":"USD"}}, + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"1.50","currencyCode":"USD"}}, + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"3.00","currencyCode":"USD"},"code":"SAVE"}, + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"2.00","currencyCode":"USD"},"code":"SAVE"} + ] + """, + totalAmount: "89.50" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.first { $0.label == "order_summary.discount".localizedString }?.amount, NSDecimalNumber(string: "-5.50")) + XCTAssertEqual(items.first { $0.label == "SAVE" }?.amount, NSDecimalNumber(string: "-5.00")) + XCTAssertEqual(items.filter { $0.amount.decimalValue < 0 }.count, 2) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "89.50")) + } + + func testZeroAllocationDoesNotAddDiscountSummaryRow() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"0.00","currencyCode":"USD"},"code":"ZERO"}] + """, + totalAmount: "100.00" + ) + + let discounts = try PassKitFactory.shared.createDiscountAllocations(cart: cart) + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(discounts.first?.amount, 0) + XCTAssertFalse(items.contains { $0.label == "ZERO" }) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "100.00")) + } + + func testEmptyApplicationsDoNotAddDiscounts() throws { + let cart = try makeDiscountCart( + applications: "[]", + totalAmount: "100.00" + ) + + XCTAssertTrue(try PassKitFactory.shared.createDiscountAllocations(cart: cart).isEmpty) + } + + func testSummaryUsesAuthoritativeCartTotal() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"10.00","currencyCode":"USD"},"code":"SAVE10"}] + """, + totalAmount: "95.25" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.last?.label, "Test Shop") + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "95.25")) + } + + func testFreeShippingDeductsShippingApplicationOnce() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"10.00","currencyCode":"USD"},"code":"FREESHIP"}] + """, + shippingGroups: [("10.00", false)], + totalAmount: "100.00" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.first { $0.label == "order_summary.shipping".localizedString }?.amount, NSDecimalNumber(string: "10.00")) + XCTAssertEqual(items.first { $0.label == "FREESHIP" }?.amount, NSDecimalNumber(string: "-10.00")) + XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 100) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "100.00")) + } + + func testPartialShippingDiscountPreservesEstimatedShippingCost() throws { + let cart = try makeDiscountCart( + applications: """ + [{"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"4.00","currencyCode":"USD"},"code":"SHIP4"}] + """, + shippingGroups: [("10.00", false)], + totalAmount: "106.00" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + let shippingMethods = PassKitFactory.shared.createShippingMethods(deliveryGroups: cart.deliveryGroups.nodes) + + XCTAssertEqual(items.first { $0.label == "order_summary.shipping".localizedString }?.amount, NSDecimalNumber(string: "10.00")) + XCTAssertEqual(items.first { $0.label == "SHIP4" }?.amount, NSDecimalNumber(string: "-4.00")) + XCTAssertEqual(shippingMethods.first?.amount, NSDecimalNumber(string: "10.00")) + XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 106) + } + + func testCombinedProductAndShippingCodeAcrossDeliveryGroupsCountsEachApplicationOnce() throws { + let cart = try makeDiscountCart( + applications: """ + [ + {"targetType":"LINE_ITEM","totalAllocatedAmount":{"amount":"5.00","currencyCode":"USD"},"code":"COMBINED"}, + {"targetType":"SHIPPING_LINE","totalAllocatedAmount":{"amount":"4.00","currencyCode":"USD"},"code":"COMBINED"} + ] + """, + shippingGroups: [("8.00", false), ("2.00", true)], + totalAmount: "101.00" + ) + + let items = PassKitFactory.shared.mapToApplePayLineItems(cart: cart, merchantName: "Test Shop") + + XCTAssertEqual(items.first { $0.label == "order_summary.shipping_one_time_purchase".localizedString }?.amount, NSDecimalNumber(string: "8.00")) + XCTAssertEqual(items.first { $0.label == "order_summary.shipping_subscription".localizedString }?.amount, NSDecimalNumber(string: "2.00")) + XCTAssertEqual(items.filter { $0.label == "COMBINED" }.count, 1) + XCTAssertEqual(items.first { $0.label == "COMBINED" }?.amount, NSDecimalNumber(string: "-9.00")) + XCTAssertEqual(items.dropLast().reduce(Decimal(0)) { $0 + $1.amount.decimalValue }, 101) + XCTAssertEqual(items.last?.amount, NSDecimalNumber(string: "101.00")) + } + + private func makeDiscountCart( + applications: String, + lineSubtotals: [String] = ["100.00"], + shippingGroups: [(amount: String, subscription: Bool)] = [], + totalAmount: String + ) throws -> StorefrontAPI.Cart { + let lines = lineSubtotals.enumerated().map { index, subtotal in + """ + { + "id":"gid://shopify/CartLine/\(index)", + "quantity":2, + "merchandise":null, + "cost":{ + "totalAmount":{"amount":"\(subtotal)","currencyCode":"USD"}, + "subtotalAmount":{"amount":"\(subtotal)","currencyCode":"USD"} + } + } + """ + } + let groups = shippingGroups.enumerated().map { index, group in + let option = """ + { + "handle":"shipping-\(index)", + "title":"Shipping \(index)", + "deliveryMethodType":"SHIPPING", + "estimatedCost":{"amount":"\(group.amount)","currencyCode":"USD"} + } + """ + return """ + { + "id":"gid://shopify/CartDeliveryGroup/\(index)", + "groupType":"\(group.subscription ? "SUBSCRIPTION" : "ONE_TIME_PURCHASE")", + "deliveryOptions":[\(option)], + "selectedDeliveryOption":\(option) + } + """ + } + let json = """ + { + "id":"gid://shopify/Cart/discount-test", + "checkoutUrl":"https://test-shop.myshopify.com/checkout", + "totalQuantity":\(lineSubtotals.count * 2), + "deliveryGroups":{"nodes":[\(groups.joined(separator: ","))]}, + "lines":{"nodes":[\(lines.joined(separator: ","))]}, + "cost":{"totalAmount":{"amount":"\(totalAmount)","currencyCode":"USD"}}, + "discountApplications":\(applications) + } + """ + return try JSONDecoder().decode(StorefrontAPI.Cart.self, from: Data(json.utf8)) + } } From 85b561ec939568b854d4be64d717e28308ae9303 Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Fri, 18 Sep 2026 09:11:05 +0100 Subject: [PATCH 5/5] Align Storefront API docs and demo fallback with 2026-07 --- platforms/android/README.md | 6 +++--- platforms/swift/README.md | 6 +++--- .../CheckoutKitSwiftDemo/Sources/Lib/InfoDictionary.swift | 2 +- platforms/web/README.md | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/platforms/android/README.md b/platforms/android/README.md index 1741b0bbd..f9eb580f7 100644 --- a/platforms/android/README.md +++ b/platforms/android/README.md @@ -72,13 +72,13 @@ dependencies { Checkout Kit presents a standard Shopify checkout URL. The common flow is: -1. Create or update a cart with the [Storefront GraphQL API](https://shopify.dev/docs/api/storefront), for example with [`cartCreate`](https://shopify.dev/docs/api/storefront/2026-04/mutations/cartCreate) and related cart mutations. -2. Read the cart's [`checkoutUrl`](https://shopify.dev/docs/api/storefront/2026-04/objects/Cart#field-cart-checkouturl). +1. Create or update a cart with the [Storefront GraphQL API](https://shopify.dev/docs/api/storefront), for example with [`cartCreate`](https://shopify.dev/docs/api/storefront/2026-07/mutations/cartCreate) and related cart mutations. +2. Read the cart's [`checkoutUrl`](https://shopify.dev/docs/api/storefront/2026-07/objects/Cart#field-cart-checkouturl). 3. Pass that URL, or a [cart permalink](https://help.shopify.com/en/manual/products/details/cart-permalink), to Checkout Kit. You can use any GraphQL client. The sample app uses Apollo Kotlin and is a complete reference for a modern Storefront API cart flow. -For production use, see the [Storefront API GraphiQL Explorer](https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/getting-started) for schema exploration and the [`cartCreate`](https://shopify.dev/docs/api/storefront/2026-04/mutations/cartCreate) mutation reference for the full input shape, including buyer identity, attributes, discount codes, delivery addresses, and delivery options. +For production use, see the [Storefront API GraphiQL Explorer](https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/getting-started) for schema exploration and the [`cartCreate`](https://shopify.dev/docs/api/storefront/2026-07/mutations/cartCreate) mutation reference for the full input shape, including buyer identity, attributes, discount codes, delivery addresses, and delivery options. ## Present checkout diff --git a/platforms/swift/README.md b/platforms/swift/README.md index 2b27a8e6a..270690c4d 100644 --- a/platforms/swift/README.md +++ b/platforms/swift/README.md @@ -91,13 +91,13 @@ pod "ShopifyCheckoutKit/AcceleratedCheckouts", "4.0.0-alpha.6" Checkout Kit presents a standard Shopify checkout URL. The common flow is: -1. Create or update a cart with the [Storefront GraphQL API](https://shopify.dev/docs/api/storefront), for example with [`cartCreate`](https://shopify.dev/docs/api/storefront/2026-04/mutations/cartCreate) and related cart mutations. -2. Read the cart's [`checkoutUrl`](https://shopify.dev/docs/api/storefront/2026-04/objects/Cart#field-cart-checkouturl). +1. Create or update a cart with the [Storefront GraphQL API](https://shopify.dev/docs/api/storefront), for example with [`cartCreate`](https://shopify.dev/docs/api/storefront/2026-07/mutations/cartCreate) and related cart mutations. +2. Read the cart's [`checkoutUrl`](https://shopify.dev/docs/api/storefront/2026-07/objects/Cart#field-cart-checkouturl). 3. Pass that URL, or a [cart permalink](https://help.shopify.com/en/manual/products/details/cart-permalink), to Checkout Kit. You can use any GraphQL client. The sample app uses Apollo iOS and is a complete reference for a modern Storefront API integration. -For production use, see the [Storefront API GraphiQL Explorer](https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/getting-started) for schema exploration and the [`cartCreate`](https://shopify.dev/docs/api/storefront/2026-04/mutations/cartCreate) mutation reference for the full input shape, including buyer identity, attributes, discount codes, and delivery preferences. +For production use, see the [Storefront API GraphiQL Explorer](https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/getting-started) for schema exploration and the [`cartCreate`](https://shopify.dev/docs/api/storefront/2026-07/mutations/cartCreate) mutation reference for the full input shape, including buyer identity, attributes, discount codes, and delivery preferences. ## Present checkout diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Lib/InfoDictionary.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Lib/InfoDictionary.swift index 548a0e608..856ae1f9f 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Lib/InfoDictionary.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Lib/InfoDictionary.swift @@ -42,7 +42,7 @@ final class InfoDictionary: Sendable { fatalError("Missing required configuration. Check your info.plist.") } - let apiVersion = infoPlist["API_VERSION"] as? String ?? "2026-04" + let apiVersion = infoPlist["API_VERSION"] as? String ?? "2026-07" self.apiVersion = apiVersion self.address1 = address1 diff --git a/platforms/web/README.md b/platforms/web/README.md index e9aa93d55..94398f32f 100644 --- a/platforms/web/README.md +++ b/platforms/web/README.md @@ -249,14 +249,14 @@ declare module 'react' { To present checkout you first need a checkout URL. The most common way is to use the [Storefront GraphQL API](https://shopify.dev/docs/api/storefront) to assemble a cart (via `cartCreate` and related mutations) and read the -[`checkoutUrl`](https://shopify.dev/docs/api/storefront/2026-04/objects/Cart#field-cart-checkouturl) +[`checkoutUrl`](https://shopify.dev/docs/api/storefront/2026-07/objects/Cart#field-cart-checkouturl) field. Alternatively, a [cart permalink](https://help.shopify.com/en/manual/products/details/cart-permalink) can be provided. ```ts const response = await fetch( - 'https://your-store.myshopify.com/api/2026-04/graphql.json', + 'https://your-store.myshopify.com/api/2026-07/graphql.json', { method: 'POST', headers: { @@ -300,7 +300,7 @@ checkout.src = data.cartCreate.cart.checkoutUrl; For production use, see the [Storefront API GraphiQL Explorer](https://shopify.dev/docs/storefronts/headless/building-with-the-storefront-api/getting-started) for schema exploration and the -[`cartCreate`](https://shopify.dev/docs/api/storefront/2026-04/mutations/cartCreate) +[`cartCreate`](https://shopify.dev/docs/api/storefront/2026-07/mutations/cartCreate) mutation reference for the full input shape (buyer identity, attributes, discount codes, delivery preferences, etc.).