From 83d196ecb1d439486505e583aa1c2f58ebda0629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Tue, 17 Mar 2026 17:31:41 +0100 Subject: [PATCH] Add Shopify Connector documentation hierarchy Bootstrap CLAUDE.md and docs/ for the Shopify Connector app covering data model, business logic, extensibility, and patterns at app level, plus per-module CLAUDE.md for 17 subfolders. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Apps/W1/Shopify/App/CLAUDE.md | 60 +++++++++ .../W1/Shopify/App/docs/business-logic.md | 124 ++++++++++++++++++ src/Apps/W1/Shopify/App/docs/data-model.md | 107 +++++++++++++++ src/Apps/W1/Shopify/App/docs/extensibility.md | 99 ++++++++++++++ src/Apps/W1/Shopify/App/docs/patterns.md | 119 +++++++++++++++++ .../W1/Shopify/App/src/Base/docs/CLAUDE.md | 22 ++++ .../Shopify/App/src/Catalogs/docs/CLAUDE.md | 19 +++ .../Shopify/App/src/Companies/docs/CLAUDE.md | 20 +++ .../Shopify/App/src/Customers/docs/CLAUDE.md | 20 +++ .../App/src/Document Links/docs/CLAUDE.md | 17 +++ .../Shopify/App/src/Inventory/docs/CLAUDE.md | 20 +++ .../W1/Shopify/App/src/Logs/docs/CLAUDE.md | 19 +++ .../Shopify/App/src/Metafields/docs/CLAUDE.md | 20 +++ .../App/src/Order Fulfillments/docs/CLAUDE.md | 18 +++ .../App/src/Order Refunds/docs/CLAUDE.md | 17 +++ .../docs/CLAUDE.md | 17 +++ .../App/src/Order Returns/docs/CLAUDE.md | 17 +++ .../App/src/Order handling/docs/CLAUDE.md | 19 +++ .../App/src/Order handling/docs/data-model.md | 54 ++++++++ .../Shopify/App/src/Payments/docs/CLAUDE.md | 17 +++ .../Shopify/App/src/Products/docs/CLAUDE.md | 19 +++ .../App/src/Products/docs/data-model.md | 54 ++++++++ .../Shopify/App/src/Shipping/docs/CLAUDE.md | 17 +++ .../App/src/Transactions/docs/CLAUDE.md | 17 +++ 24 files changed, 932 insertions(+) create mode 100644 src/Apps/W1/Shopify/App/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/docs/business-logic.md create mode 100644 src/Apps/W1/Shopify/App/docs/data-model.md create mode 100644 src/Apps/W1/Shopify/App/docs/extensibility.md create mode 100644 src/Apps/W1/Shopify/App/docs/patterns.md create mode 100644 src/Apps/W1/Shopify/App/src/Base/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Catalogs/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Companies/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Customers/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Document Links/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Inventory/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Logs/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Metafields/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Order Fulfillments/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Order Refunds/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Order Return Refund Processing/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Order Returns/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Order handling/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Order handling/docs/data-model.md create mode 100644 src/Apps/W1/Shopify/App/src/Payments/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Products/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Products/docs/data-model.md create mode 100644 src/Apps/W1/Shopify/App/src/Shipping/docs/CLAUDE.md create mode 100644 src/Apps/W1/Shopify/App/src/Transactions/docs/CLAUDE.md diff --git a/src/Apps/W1/Shopify/App/CLAUDE.md b/src/Apps/W1/Shopify/App/CLAUDE.md new file mode 100644 index 0000000000..f78a701ebe --- /dev/null +++ b/src/Apps/W1/Shopify/App/CLAUDE.md @@ -0,0 +1,60 @@ +# Shopify Connector + +Bidirectional sync between Shopify stores and Business Central. Imports orders, customers, and companies from Shopify; exports products, inventory, and shipments to Shopify. All API communication uses Shopify's GraphQL Admin API, not REST. The connector supports multiple Shopify stores per BC company, each configured independently through the Shpfy Shop table (30102). + +## Quick reference + +- **ID range**: 30100--30460 +- **Dependencies**: none (zero entries in `app.json`) + +## How it works + +Everything flows through the Shop record. Each Shop represents one Shopify store connection and carries 60+ configuration fields that control sync direction, customer mapping strategy, product status rules, inventory calculation method, return/refund processing, and more. The Shop is the god object -- nearly every codeunit takes a Shop record as its entry point, and most design decisions are Shop-level settings rather than per-entity configuration. + +The sync model is asymmetric by design. Products flow primarily BC-to-Shopify (items are the source of truth), while orders flow Shopify-to-BC (Shopify is the order capture system). Customer sync supports both directions but import is automatic while export requires manual "Add to Shopify" actions. Inventory is export-only: BC calculates stock using a pluggable strategy interface and pushes adjustments to Shopify locations. + +All Shopify API calls are GraphQL, encapsulated through the `Shpfy IGraphQL` interface. The `ShpfyGraphQLType` enum (30111) defines 143 query types, each implemented by a dedicated `Shpfy GQL *` codeunit. The `ShpfyGraphQLQueries` codeunit (30154) resolves enum values to query text via interface dispatch, with template parameter replacement (`{{param}}`). This design means you can override any GraphQL query by subscribing to events on `ShpfyGraphQLQueries` without touching the original codeunit. + +The connector does not attempt real-time sync. It uses batch pulls (cursor-based pagination over GraphQL connections) and optional webhooks for order notifications. There is no message queue, no retry infrastructure beyond the standard BC job queue, and no conflict resolution beyond hash-based detection that flags orders for manual review. + +## Structure + +- `src/Base/` -- Shop table, installer, shared enums, cue/activities for role centers +- `src/GraphQL/` -- IGraphQL interface, 143 GQL codeunits (one per query), rate limiter +- `src/Products/` -- Product/Variant tables, import/export codeunits, price calculation +- `src/Order handling/` -- Order header/line tables, import, mapping, create sales document +- `src/Order Fulfillments/` -- Fulfillment orders (modern per-location API) and actual shipments +- `src/Order Returns/` and `src/Order Refunds/` -- Separate return and refund models +- `src/Order Return Refund Processing/` -- IReturnRefundProcess interface and strategy implementations +- `src/Customers/` -- Customer table, mapping interfaces (email/phone, bill-to, default) +- `src/Companies/` -- B2B company and company location tables, separate mapping strategies +- `src/Inventory/` -- Shop locations, stock calculation interfaces, inventory sync +- `src/Transactions/` -- Payment transactions and gateway mappings +- `src/Payments/` -- Payouts for bank reconciliation +- `src/Gift Cards/` -- Gift card tracking (product when sold, payment method when redeemed) +- `src/Metafields/` -- Typed custom fields with namespace and owner-type polymorphism +- `src/Document Links/` -- N:M junction between Shopify documents and BC documents +- `src/Logs/` -- Data capture (raw JSON), skipped records, communication events +- `src/Bulk Operations/` -- Bulk mutation support for high-volume price updates +- `src/Catalogs/` -- B2B catalog and market-specific pricing +- `src/Webhooks/` -- Webhook subscription management for order notifications + +## Documentation + +- [docs/data-model.md](docs/data-model.md) -- How the data fits together +- [docs/business-logic.md](docs/business-logic.md) -- Processing flows and gotchas +- [docs/extensibility.md](docs/extensibility.md) -- Extension points and how to customize +- [docs/patterns.md](docs/patterns.md) -- Recurring code patterns (and legacy ones to avoid) + +## Things to know + +- Products link to BC Items via `Item SystemId` (a Guid), not Item No. This survives item renumbering. The `Item No.` field on the Product table is a FlowField that looks up through SystemId. +- The Variant table (30129) has three pairs of option name/value fields (`Option 1 Name`/`Option 1 Value` through 3). Shopify's 3-option limit is a hard constraint reflected in the schema. +- Order import always creates the header first, then retrieves and inserts lines, related records (tax, shipping, transactions, fulfillment orders, returns, refunds), and finally adjusts line quantities for refunds. The sequence matters because refund line quantities are subtracted from order line quantities in place. +- The `Line Items Redundancy Code` on the order header is a hash of concatenated line IDs. It is used for conflict detection when re-importing an already-processed order -- if the hash changes, the order is flagged as conflicting. +- Customer mapping uses interface dispatch: the Shop's `Customer Mapping Type` enum selects which `ICustomerMapping` implementation runs. But there is a fallback -- if both Name and Name2 are empty, it always falls back to "By EMail/Phone" regardless of the setting. +- `Shpfy Data Capture` (30114) stores raw JSON blobs linked to any record via table ID and SystemId. This is the primary debugging tool -- when something goes wrong with an import, look at the captured JSON. +- The `Shpfy Skipped Record` table (30159) logs every record the connector deliberately skipped during sync, with a reason. Check this before assuming a sync bug. +- Negative IDs on `Shpfy Customer Address` indicate addresses created by BC (not imported from Shopify). This convention prevents ID collisions. +- The `Return and Refund Process` field on the Shop controls whether returns and refunds are imported at all, and whether credit memos are auto-created. The `IReturnRefundProcess` interface routes to different implementations based on this enum. +- Bulk operations (used for price-only sync) attempt to use Shopify's bulk mutation API first, then fall back to individual GraphQL calls if the bulk operation fails. diff --git a/src/Apps/W1/Shopify/App/docs/business-logic.md b/src/Apps/W1/Shopify/App/docs/business-logic.md new file mode 100644 index 0000000000..94b6cec263 --- /dev/null +++ b/src/Apps/W1/Shopify/App/docs/business-logic.md @@ -0,0 +1,124 @@ +# Business logic + +## Product sync + +Product sync is direction-dependent, controlled by the Shop's `Sync Item` option (To Shopify / From Shopify). The `ShpfySyncProducts.Codeunit.al` entry point dispatches to either `ShpfyProductExport` or `ShpfyProductImport` based on this setting. There is also a price-only mode (`SetOnlySyncPriceOn`) that skips all non-price fields and uses bulk operations for efficiency. + +### Export (BC to Shopify) + +The export flow in `ShpfyProductExport.Codeunit.al` starts by filtering `Shpfy Product` records that have a non-empty `Item SystemId` and belong to the current shop. The `OnAfterProductsToSynchronizeFiltersSet` event fires here, allowing subscribers to add additional filters. + +For each product, the flow calls `UpdateProductData`, which loads the linked BC Item, calls `FillInProductFields` to populate the temp product record (title from item translation or description, vendor from vendor table, product type from item category, HTML body from extended text + marketing text + attributes), then compares hashes to detect changes. + +The blocked-item handling is worth noting: when an Item is blocked, the product's status is set according to the Shop's `Action for Removed Products` setting (archive or draft). The `IRemoveProductAction` interface handles the status transition, and the `ICreateProductStatusValue` interface determines the initial status when creating new products. + +Variant export iterates all Item Variants plus, if `UoM as Variant` is enabled, all Item Units of Measure. Each variant gets a price calculation, SKU mapping (configurable: barcode, item no., vendor item no.), barcode lookup, and weight computation. Blocked or sales-blocked variants are skipped and logged to the Skipped Records table. + +Item attributes mapped as product options occupy variant option slots. Since Shopify caps at 3 options and UoM-as-variant consumes one slot, attributes are limited to 2 (or 3 with UoM disabled). The `VerifyNoItemAttributesAsOptions` check in the Shop table enforces this constraint. + +```mermaid +flowchart TD + A[Filter products with Item SystemId] --> B{Can Update Shopify Products?} + B -- No --> Z[Skip] + B -- Yes --> C[Load BC Item] + C --> D{Item Blocked?} + D -- Yes --> E[Set status per Action for Removed Products] + D -- No --> F[FillInProductFields] + F --> G[Compare hashes -- image, tags, description] + G --> H{Changed?} + H -- No --> Z + H -- Yes --> I[Update product via GraphQL] + I --> J[Process variants] + J --> K{Variant blocked?} + K -- Yes --> L[Log to Skipped Records] + K -- No --> M[Fill variant data + price calc] + M --> N[Create or update variant via GraphQL] + N --> O[Sync metafields and translations] +``` + +### Import (Shopify to BC) + +Import in `ShpfySyncProducts.Codeunit.al` retrieves all product IDs from Shopify via `ProductApi.RetrieveShopifyProductIds`, then compares `Updated At` timestamps against local records. Products where the Shopify timestamp is newer than both `Updated At` and `Last Updated by BC` are queued into a temp table. Products already up-to-date are silently skipped. + +For each changed product, `ShpfyProductImport` runs in its own commit scope. If it fails, the error is captured and the loop continues with the next product. This per-record isolation prevents one bad product from blocking the entire sync. + +## Order import + +Order import is the connector's most complex flow, orchestrated by `ShpfyImportOrder.Codeunit.al`. The process handles first-time import, re-import of changed orders, and conflict detection for already-processed orders. + +```mermaid +flowchart TD + A[Receive Order ID] --> B[EnsureOrderHeaderExists] + B --> C[Retrieve order JSON via GraphQL] + C --> D[Retrieve and parse order lines] + D --> E{Already processed in BC?} + E -- Yes --> F{Hash/qty/shipping changed?} + F -- Yes --> G[Flag as conflicting] + F -- No --> H[Continue import] + E -- No --> H + H --> I[SetOrderHeaderValuesFromJson -- 50+ fields] + I --> J[SetAndCreateRelatedRecords] + J --> J1[Tax lines] + J --> J2[Shipping charges] + J --> J3[Transactions] + J --> J4[Fulfillment orders] + J --> J5[Returns -- if IReturnRefundProcess says so] + J --> J6[Refunds -- if IReturnRefundProcess says so] + J --> K[Insert order lines + data capture] + K --> L[Compute Line Items Redundancy Code hash] + L --> M[ConsiderRefundsInQuantityAndAmounts] + M --> N[Delete zero-quantity lines] + N --> O{All lines fulfilled + closed?} + O -- Yes --> P[Close order via GraphQL] + O -- No --> Q[Check if Shopify invoice exists] + Q -- Yes --> R[Mark as processed] + Q -- No --> S[Order ready for mapping] +``` + +The conflict detection in `IsImportedOrderConflictingExistingOrder` checks three conditions: (1) whether `currentSubtotalLineItemsQuantity` increased, (2) whether the line items redundancy hash changed, and (3) whether the shipping charges amount changed. Any mismatch sets `Has Order State Error` and `Has Error` on the header, blocking auto-processing until manually resolved. + +The refund adjustment step (`ConsiderRefundsInQuantityAndAmounts`) is particularly important. It iterates all order lines, sums refund line quantities for each, and subtracts them directly from the order line's `Quantity`. It also adjusts the header's total amounts. This means the order lines in BC reflect net quantities after refunds, not the original Shopify quantities. Lines reduced to zero are deleted entirely. + +## Order mapping and creation + +After import, `ShpfyOrderMapping.Codeunit.al` maps the Shopify order to BC entities. The mapping splits based on B2B status: + +- **B2C orders** use customer mapping through the `ICustomerMapping` interface, selected by the Shop's `Customer Mapping Type`. The fallback logic is notable: if both `Name` and `Name2` on the order are empty, the mapper always uses "By EMail/Phone" regardless of the configured strategy. +- **B2B orders** use company mapping through `ICompanyMapping`, routing through the Shop's company settings. + +For each order line, `MapVariant` resolves the Shopify variant to a BC Item + Item Variant. Tip lines check that `Tip Account` is configured, gift card lines check `Sold Gift Card Account`. + +Once mapping succeeds, `ShpfyCreateSalesDocHeader.Codeunit.al` creates the actual Sales Order (or Sales Invoice, depending on configuration). The `Auto Release Sales Orders` shop setting controls whether the document is immediately released. + +## Customer sync + +Customer import in `ShpfySyncCustomers.Codeunit.al` retrieves customer IDs from Shopify via GraphQL, then processes each through `ShpfyCustomerImport`. The mapping in `ShpfyCustomerMapping.Codeunit.al` uses a two-phase approach: + +1. **FindMapping** checks if the Shopify customer already has a `Customer SystemId`. If the linked BC customer no longer exists, it clears the link and proceeds to step 2. +2. **DoFindMapping** searches BC customers by email (case-insensitive filter using `@` prefix) and phone number (digits-only fuzzy matching via `CreatePhoneFilter`). The phone matching extracts only digits from both the Shopify phone and BC phone fields, handling format differences like +1 (555) 123-4567 vs 5551234567. + +Customer export is not automatic. The old `Export Customer To Shopify` boolean was removed in v27 (the field definition is still guarded by `#if not CLEANSCHEMA27` in the Shop table). The replacement is a manual "Add to Shopify" action on the Shopify Customers page. + +## Inventory sync + +Inventory sync in the Inventory folder is export-only: it calculates BC stock and pushes adjustments to Shopify. For each Shop Location, the connector applies the location's `Location Filter` to restrict which BC locations contribute to the stock calculation, then calls the configured stock calculation interface. + +The stock calculation is a three-layer interface hierarchy. `Shpfy IStock Available` answers a boolean "can this type have stock?" question. `Shpfy Stock Calculation` provides the basic `GetStock(Item)` method. `Shpfy Extended Stock Calculation` extends it with `GetStock(Item, ShopLocation)` for location-aware calculations. The Shop's stock calculation enum selects the active implementation. + +The connector sends stock adjustments (deltas) to Shopify via the `ModifyInventory` GraphQL mutation, not absolute values. It compares the calculated BC stock against the `Shopify Stock` field on `Shpfy Shop Inventory` to determine the delta. + +## Webhooks + +The connector supports webhooks for real-time order notifications, managed by `ShpfyWebhooksMgt.Codeunit.al`. When the `Order Created Webhooks` setting is enabled on the Shop, the connector registers a webhook subscription with Shopify. Incoming webhook payloads trigger `ShpfyWebhookNotification.Codeunit.al`, which queues the order for import. + +Webhook subscriptions are cleaned up when the shop is disabled (the `Enabled` field's OnValidate trigger calls `WebhooksMgt.DisableBulkOperationsWebhook`). + +## Return and refund processing + +The `Return and Refund Process` enum on the Shop controls the behavior through the `IReturnRefundProcess` interface. The interface has three key methods: + +- `IsImportNeededFor(SourceDocumentType)` -- whether to import returns/refunds at all +- `CanCreateSalesDocumentFor(SourceDocumentType, SourceDocumentId)` -- whether a credit memo can be created +- `CreateSalesDocument(SourceDocumentType, SourceDocumentId)` -- creates the actual credit memo + +The "Import Only" setting imports returns and refunds for visibility but does not create credit memos. "Auto Create Credit Memo" requires `Auto Create Orders` to be enabled (enforced by validation on both fields). This two-field coupling is a common source of configuration errors. diff --git a/src/Apps/W1/Shopify/App/docs/data-model.md b/src/Apps/W1/Shopify/App/docs/data-model.md new file mode 100644 index 0000000000..889cfbd26e --- /dev/null +++ b/src/Apps/W1/Shopify/App/docs/data-model.md @@ -0,0 +1,107 @@ +# Data model + +The Shopify Connector's data model mirrors Shopify's own domain model more than it mirrors BC's. This is deliberate -- the connector needs to faithfully represent what Shopify sends, then map it to BC structures during order processing. The result is a parallel set of tables that shadow Shopify entities, linked to BC records through SystemId guids and document number references. + +## Product catalog + +The product hierarchy follows Shopify's structure: a Product contains Variants, and Variants are the purchasable units. The critical design decision is that the Product table (30127) links to BC Item through `Item SystemId` (a Guid), not through `Item No.`. The `Item No.` field exists as a FlowField that resolves through SystemId. This means products survive item renumbering without breaking the mapping. + +The `Has Variants` flag on the Product controls whether the connector creates BC Item Variants or treats the Shopify product as a single-variant item. Shopify always has at least one variant per product, but when `Has Variants` is false, the connector maps the sole variant directly to the Item without creating an Item Variant record. + +Variants carry three option name/value pairs (`Option 1 Name`/`Option 1 Value` through 3), reflecting Shopify's hard limit of 3 product options. When item attributes are mapped as options, this limit constrains how many attributes can participate. The `UoM as Variant` shop setting consumes one of these option slots for unit of measure, further reducing available slots. + +Change detection uses integer hash fields on the Product: `Image Hash`, `Tags Hash`, and `Description Html Hash`. During export, the connector compares the current hash against the stored one to skip unchanged products. These are plain integer hashes (via `ShpfyHash` codeunit), not cryptographic -- collisions are possible but rare enough for change detection purposes. + +The `Shpfy Product Collection` table (30127... actually a separate table) provides an N:M mapping between products and Shopify collections, keyed by Product Collection ID and Product ID. + +```mermaid +erDiagram + SHOP ||--o{ PRODUCT : "scoped by" + PRODUCT ||--|{ VARIANT : "contains" + PRODUCT }o--o{ PRODUCT_COLLECTION : "categorized in" + VARIANT ||--o| SHOP_INVENTORY : "stock tracked per location" + VARIANT }o--|| ITEM : "linked via SystemId" +``` + +## Order lifecycle + +Orders are the most complex domain. The `Shpfy Order Header` (30118) stores approximately 50 active fields covering three address sets (sell-to, ship-to, bill-to), dual currency amounts (shop currency and presentment currency), B2B fields (`Company Id`, `PO Number`), and processing state. + +The dual-currency design stores every monetary amount twice: once in the shop's base currency (`Total Amount`, `Subtotal Amount`, `VAT Amount`) and once in the customer's presentment currency (`Presentment Total Amount`, etc.). This matters for multi-currency storefronts where the customer pays in one currency but the merchant books revenue in another. + +Order Lines (30119) link to the header via `Shopify Order Id`. Each line carries its own variant reference, quantity, and amounts. The line also has a `Gift Card` flag and a `Tip` flag -- these are not normal product lines and route to different GL accounts during sales document creation. + +The `Line Items Redundancy Code` on the header is an integer hash of the concatenated, pipe-separated line IDs, computed by the `ShpfyHash` codeunit in `ShpfyImportOrder.Codeunit.al`. When re-importing an order that was already processed into a BC sales document, the connector compares this hash against the stored value. A mismatch (meaning lines were added or removed in Shopify after BC processed the order) triggers a conflict flag. + +Fulfillment Orders (`Shpfy FulFillment Order Header`, 30143) represent Shopify's modern fulfillment model where each fulfillment order is scoped to a single location. These are distinct from actual Order Fulfillments (`Shpfy Order Fulfillment`, 30135), which represent completed shipments with tracking info. The connector imports both: fulfillment orders for location-aware order routing, and fulfillments for shipment tracking. + +Returns and Refunds are deliberately separate entities. The `Shpfy Return Header`/`Return Line` tables track customer return requests, while `Shpfy Refund Header`/`Refund Line` track financial adjustments. A return can exist without a refund (customer sends item back, refund pending) and a refund can exist without a return (price adjustment, no physical return). The `IReturnRefundProcess` interface controls whether these are imported and whether credit memos are auto-created. + +```mermaid +erDiagram + ORDER_HEADER ||--|{ ORDER_LINE : "contains" + ORDER_HEADER ||--o{ TAX_LINE : "has" + ORDER_HEADER ||--o{ FULFILLMENT_ORDER : "routed as" + ORDER_HEADER ||--o{ ORDER_FULFILLMENT : "shipped via" + ORDER_HEADER ||--o{ RETURN_HEADER : "returned through" + ORDER_HEADER ||--o{ REFUND_HEADER : "refunded via" +``` + +## Customer and company + +The `Shpfy Customer` table (30105) stores Shopify customer data with a `Customer SystemId` guid linking to BC's Customer table. The `Shpfy Customer Address` table uses a BigInteger `Id` as its primary key. Addresses created by BC (during customer export) use negative IDs to avoid colliding with Shopify-assigned positive IDs -- a simple but effective convention. + +B2B support adds the `Shpfy Company` table (30150) and `Shpfy Company Location` table. A Company can have multiple locations, and each location can map to a different BC customer. This is unusual -- in BC, a customer is typically a single entity, but in Shopify B2B, a company location is the billing/shipping unit. The `Customer SystemId` field appears on both Company and Company Location, allowing mapping at either level. + +Customer Templates (`Shpfy Customer Template`, keyed by Shop Code + Country/Region Code) control how new BC customers are created during import. The template provides a default customer number and template code per country, giving per-market configuration. + +```mermaid +erDiagram + CUSTOMER ||--o{ CUSTOMER_ADDRESS : "has" + CUSTOMER }o--o| BC_CUSTOMER : "mapped via SystemId" + COMPANY ||--|{ COMPANY_LOCATION : "operates from" + COMPANY_LOCATION }o--o| BC_CUSTOMER : "mapped via SystemId" + CUSTOMER_TEMPLATE }o--|| SHOP : "configured per" +``` + +## Payments and transactions + +The `Shpfy Order Transaction` table (30133) stores payment events with a `Type` enum (authorization, capture, refund, etc.) and a `Gateway` text field for the payment provider name. Transactions form parent-child chains -- a capture references its authorization, a refund references its capture. The combination of Gateway + card brand determines which BC Payment Method is used during sales document creation. + +Payouts (`Shpfy Payout` table) represent bank settlement records from Shopify Payments, used for bank reconciliation. Gift cards (`Shpfy Gift Card` table) have a dual role: they appear as products when sold (routed to the `Sold Gift Card Account` GL account) and as payment methods when redeemed (appearing as transactions on orders). + +```mermaid +erDiagram + ORDER_HEADER ||--o{ ORDER_TRANSACTION : "paid through" + ORDER_TRANSACTION ||--o| ORDER_TRANSACTION : "parent chain" + ORDER_HEADER ||--o{ PAYMENT_GATEWAY : "gateway info" + PAYOUT ||--o{ ORDER_TRANSACTION : "settles" +``` + +## Inventory + +The `Shpfy Shop Location` table (30113) maps Shopify locations to BC location filters. Each Shop Location has a `Location Filter` field (a BC location filter expression) and a `Default Location Code`. The filter determines which BC locations contribute stock when calculating available inventory for that Shopify location. + +The `Shpfy Shop Inventory` table (30112) stores per-variant-per-location stock snapshots, keyed by Shop Code + Product Id + Variant Id + Location Id. The `Shopify Stock` field records the last-known Shopify stock level, and the connector sends adjustments (deltas) rather than absolute values when syncing inventory to Shopify. + +Stock calculation is pluggable through interfaces. `Shpfy Stock Calculation` provides a simple `GetStock(Item)` method, while `Shpfy Extended Stock Calculation` extends it with a `GetStock(Item, ShopLocation)` overload that receives the location context. The `Shpfy IStock Available` interface separately answers whether a given stock calculation type can have stock at all (used to skip non-stocking scenarios). + +```mermaid +erDiagram + SHOP ||--|{ SHOP_LOCATION : "has locations" + SHOP_LOCATION ||--o{ SHOP_INVENTORY : "tracks stock" + SHOP_INVENTORY }o--|| VARIANT : "for variant" + SHOP_LOCATION }o--|| BC_LOCATION : "mapped via filter" +``` + +## Cross-cutting concerns + +**Metafields** (`Shpfy Metafield`, 30101) are Shopify's custom field mechanism. Each metafield has a Namespace, Name (key), Value, Type, and Owner Id. The `IMetafieldOwnerType` interface handles polymorphic ownership -- a metafield can belong to a product, variant, customer, or company. The `IMetafieldType` interface handles type-specific validation (the Value field's OnValidate trigger delegates to the type interface). + +**Tags** (`Shpfy Tag`, 30100) are normalized rows parsed from Shopify's CSV-formatted tag strings. They link to a parent entity via `Parent Table No.` and `Parent Id`, making them polymorphic across products, orders, customers, and companies. + +**Document Links** (`Shpfy Doc. Link To Doc.`, 30146) provide an N:M junction between Shopify documents and BC documents. The table uses enum-based polymorphism for both sides: `Shopify Document Type` and `Document Type` are enums, and the `OpenShopifyDocument`/`OpenBCDocument` methods dispatch through `IOpenShopifyDocument` and `IOpenBCDocument` interfaces respectively. + +**Data Capture** (`Shpfy Data Capture`, 30114) stores raw JSON blobs linked to any record via `Linked To Table` (table ID) and `Linked To Id` (SystemId). This is the connector's audit trail and debugging tool. When an order import goes wrong, the captured JSON shows exactly what Shopify sent. + +**Skipped Records** (`Shpfy Skipped Record`, 30159) log every record the connector deliberately chose not to sync, with a reason text. This is distinct from errors -- a skipped record means the connector's logic decided to exclude it (blocked item, unchanged product, etc.). diff --git a/src/Apps/W1/Shopify/App/docs/extensibility.md b/src/Apps/W1/Shopify/App/docs/extensibility.md new file mode 100644 index 0000000000..17c82bed9f --- /dev/null +++ b/src/Apps/W1/Shopify/App/docs/extensibility.md @@ -0,0 +1,99 @@ +# Extensibility + +The Shopify Connector offers two primary extension mechanisms: integration events (subscribe to inject behavior at specific points) and interface-based strategy selection (implement an interface, add an enum value, and the connector dispatches to your code). Most events are declared as `InternalEvent` or `IntegrationEvent` on dedicated event codeunits per domain. + +## Customize product export + +The product export pipeline in `ShpfyProductExport.Codeunit.al` fires events at several points: + +- **Filter which products sync**: Subscribe to `OnAfterProductsToSynchronizeFiltersSet` on `ShpfyProductEvents.Codeunit.al` to add filters to the `Shpfy Product` record before the export loop starts. This is the right place to exclude certain products from sync. + +- **Override product title**: `OnBeforSetProductTitle` (note the typo -- it is in the codebase) on `ShpfyProductEvents` fires before the title is assigned. Set `IsHandled` to true and modify `Title` to fully replace the title logic. `OnAfterSetProductTitle` fires after the default logic for post-processing. + +- **Override product body HTML**: `OnBeforeCreateProductBodyHtml` on `ShpfyProductEvents` lets you replace the entire HTML generation (extended text + marketing text + attributes). `OnAfterCreateProductbodyHtml` lets you modify the result. + +- **Modify product fields after population**: `OnAfterFillInShopifyProductFields` on `ShpfyProductEvents` fires after all standard fields are set, giving you a last chance to modify the temp `Shpfy Product` record before comparison and API call. + +- **Modify variant fields**: `OnAfterCreateTempShopifyVariant` fires after variant data is populated from the BC Item. + +- **Modify tags**: `OnAfterCreateTempShopifyProduct` passes the temp product, variant, and tag records, letting you add or remove tags before export. + +### Control product status on creation + +The `Status for Created Products` setting on the Shop selects an `ICreateProductStatusValue` implementation via the `Shpfy Cr. Prod. Status Value` enum. To add a custom status rule, add a new enum value with an `Implementation` attribute pointing to your codeunit that implements `ICreateProductStatusValue.GetStatus(Item): Enum "Shpfy Product Status"`. + +### Control what happens to removed products + +The `Action for Removed Products` setting uses the `IRemoveProductAction` interface. Add a new enum value to `Shpfy Remove Product Action` with your implementation of `IRemoveProductAction.RemoveProductAction(var Product)`. + +## Customize order import and creation + +Order events are centralized in `ShpfyOrderEvents.Codeunit.al`: + +- **Override customer mapping on order**: `OnBeforeMapCustomer` with the `Handled` pattern. Set `Handled := true` and populate `Bill-to Customer No.`/`Sell-to Customer No.` directly on the `ShopifyOrderHeader` record to bypass the standard mapping logic entirely. + +- **Post-process customer mapping**: `OnAfterMapCustomer` fires after the standard mapping completes, letting you adjust the result. + +- **Override shipment method/agent/payment method mapping**: Each has a Before/After pair: `OnBeforeMapShipmentMethod`/`OnAfterMapShipmentMethod`, `OnBeforeMapShipmentAgent`/`OnAfterMapShipmentAgent`, `OnBeforeMapPaymentMethod`/`OnAfterMapPaymentMethod`. The Before events support the `Handled` pattern. + +- **React to completed import**: `OnAfterImportShopifyOrderHeader` fires after the header is populated from JSON. `OnAfterCreateShopifyOrderAndLines` fires after both header and lines are fully imported and refund adjustments are applied. The `IsNew` boolean parameter distinguishes first import from re-import. + +- **Adjust refund handling**: `OnAfterConsiderRefundsInQuantityAndAmounts` on `ShpfyOrderEvents` fires after each order line's quantity is adjusted for refunds, passing the order header, order line, and refund line records. + +### Control return/refund processing + +The `Return and Refund Process` setting on the Shop selects an `IReturnRefundProcess` implementation. The interface has three methods: `IsImportNeededFor`, `CanCreateSalesDocumentFor`, and `CreateSalesDocument`. To add a custom processing strategy, extend the `Shpfy ReturnRefund ProcessType` enum with your implementation. + +The interface also uses two companion interfaces for document source resolution: `IDocumentSource` and `Shpfy Extended IDocumentSource`, found in `src/Order Return Refund Processing/Interfaces/`. + +## Customize customer mapping + +The `Customer Mapping Type` setting on the Shop selects an `ICustomerMapping` implementation. The built-in options are: + +- **By EMail/Phone** (`ShpfyCustByEmailPhone.Codeunit.al`) -- searches by email, then by digits-only phone match +- **By Bill-to Info** (`ShpfyCustByBillto.Codeunit.al`) -- matches by bill-to address fields +- **Default Customer** (`ShpfyCustByDefaultCust.Codeunit.al`) -- always returns the shop's default customer + +To add a custom mapping strategy, extend the `Shpfy Customer Mapping` enum with your codeunit implementing `ICustomerMapping.DoMapping`. + +The related `ICustomerName` interface (selected by the Shop's `Name Source` enum) controls how the customer's display name is constructed from Shopify's first/last name and company fields. Implementations include `ShpfyNameisCompanyName`, `ShpfyNameisFirstLastName`, `ShpfyNameisLastFirstName`, and `ShpfyNameisEmpty`. + +The `ICounty` and `ICountyFromJson` interfaces control how the county/province field is resolved from Shopify's address data (by code vs. by name, from JSON code vs. JSON name). The `County Source` shop setting selects the implementation. + +Customer import/export events live in `ShpfyCustomerEvents.Codeunit.al`, with `OnBeforeFindMapping`/`OnAfterFindMapping` for intercepting the mapping process. + +## Customize company mapping (B2B) + +The `Company Mapping` setting on the Shop uses the `ICompanyMapping` and `IFindCompanyMapping` interfaces in `src/Companies/Interfaces/`. Built-in strategies include mapping by email/phone (`ShpfyCompByEmailPhone`), by tax ID (`ShpfyCompByTaxId`), and by default company (`ShpfyCompByDefaultComp`). + +## Customize inventory calculation + +The stock calculation interfaces form a hierarchy: + +1. `Shpfy IStock Available` -- boolean guard: can this calculation type produce stock? +2. `Shpfy Stock Calculation` -- basic `GetStock(Item): Decimal` +3. `Shpfy Extended Stock Calculation` -- location-aware `GetStock(Item, ShopLocation): Decimal` + +To add a custom stock calculation, extend the stock calculation enum with your codeunit implementing the appropriate interface level. If your calculation is location-dependent, implement `Shpfy Extended Stock Calculation`. + +## Override GraphQL queries + +The 143 GraphQL queries are dispatched through the `IGraphQL` interface on the `ShpfyGraphQLType` enum. The `ShpfyGraphQLQueries.Codeunit.al` dispatcher fires several events: + +- `OnBeforeSetInterfaceCodeunit` -- replace which codeunit implements a given query type +- `OnBeforeGetGrapQLInfo` / `OnAfterGetGrapQLInfo` -- replace or modify the query text and expected cost +- `OnBeforeReplaceParameters` / `OnAfterReplaceParameters` -- modify parameter substitution + +Since the enum is marked `Extensible = true`, you can also add entirely new GraphQL query types by adding enum values with your `IGraphQL` implementation. + +## Document link dispatch + +The `Shpfy Doc. Link To Doc.` table uses `IOpenShopifyDocument` and `IOpenBCDocument` interfaces for polymorphic document opening. When you add a new Shopify document type or BC document type enum value, provide implementations of these interfaces so the document link page can navigate to your documents. + +## General patterns for extension + +- **IsHandled pattern**: Most Before events pass a `var Handled: Boolean` parameter. Set it to `true` to skip the default logic. Do not set it to `true` unless you fully replace the behavior -- partial handling will leave the record in an inconsistent state. + +- **Enum-implements-interface pattern**: Configuration enums on the Shop table have `implements` clauses. Extending the enum automatically makes your strategy available in the UI dropdown. The Shop table does not need modification. + +- **InternalEvent vs IntegrationEvent**: Events marked `InternalEvent` are only subscribable from within the same app (the Shopify Connector itself and its test app via `internalsVisibleTo`). Events marked `IntegrationEvent` are subscribable from any extension app. Check the attribute before planning your extension. diff --git a/src/Apps/W1/Shopify/App/docs/patterns.md b/src/Apps/W1/Shopify/App/docs/patterns.md new file mode 100644 index 0000000000..3041380927 --- /dev/null +++ b/src/Apps/W1/Shopify/App/docs/patterns.md @@ -0,0 +1,119 @@ +# Patterns + +## GraphQL encapsulation via IGraphQL + +**Problem**: The connector needs 143+ distinct GraphQL queries, each with different structure, parameters, and cost budgets. Hardcoding query strings throughout the codebase would be unmaintainable and impossible to extend. + +**Solution**: Each GraphQL query is a separate codeunit implementing the `Shpfy IGraphQL` interface (two methods: `GetGraphQL()` returns the query template, `GetExpectedCost()` returns the Shopify API cost budget). The `ShpfyGraphQLType` enum (30111) maps symbolic names to implementations. The dispatcher in `ShpfyGraphQLQueries.Codeunit.al` resolves the enum to an interface, calls it, then replaces `{{param}}` placeholders with values from a `Dictionary of [Text, Text]`. + +**Example**: `ShpfyGQLOrderHeader.Codeunit.al` returns a GraphQL query template for fetching order headers. The caller passes `{ "OrderId": "gid://shopify/Order/12345" }` as parameters. The dispatcher substitutes `{{OrderId}}` in the query text before sending it to the API. + +**Gotcha**: The dispatcher fires events at every stage (before interface resolution, before/after query retrieval, before/after parameter replacement). If you subscribe to `OnBeforeSetInterfaceCodeunit` and set `IsHandled := true` but provide a `nil` interface, you'll get a runtime error -- the dispatcher does not null-check the interface after the event. + +## Enum-implements-interface strategy selection + +**Problem**: Many behaviors need to be user-configurable from a dropdown: customer mapping strategy, stock calculation method, product status rules, return/refund processing, county formatting, customer name formatting, etc. + +**Solution**: The enum definition includes `implements` clauses that bind each value to a codeunit. The Shop table stores the enum value. Business logic resolves the enum to its interface and calls the method. No factory codeunit, no case statement -- AL's enum-to-interface dispatch handles it. + +**Example**: The `Shpfy Customer Mapping` enum has values like `"By EMail/Phone"` (implemented by `ShpfyCustByEmailPhone`), `"By Bill-to Info"` (implemented by `ShpfyCustByBillto`). In `ShpfyCustomerMapping.Codeunit.al`, the code does `IMapping := LocalShop."Customer Mapping Type"` and then `IMapping.DoMapping(...)`. Adding a new strategy means adding an enum extension value with an `Implementation` attribute -- no changes to the mapper. + +**Gotcha**: The mapper has a hardcoded fallback: if both `Name` and `Name2` are empty, it ignores the Shop setting and uses `"By EMail/Phone"` directly. This overrides any custom implementation. If your mapping strategy does not depend on name fields, empty-name orders will still bypass it. + +## Hash-based change detection + +**Problem**: Syncing every product on every run wastes API calls. The connector needs to detect which products actually changed. + +**Solution**: Integer hash fields stored on the entity record (`Image Hash`, `Tags Hash`, `Description Html Hash` on `Shpfy Product`). Before export, the connector computes the current hash and compares it to the stored value. If they match, the product is skipped. + +**Example**: In `ShpfyProductExport.Codeunit.al`, `FillInProductFields` sets `ShopifyProduct."Tags Hash" := ShopifyProduct.CalcTagsHash()`. The `CalcTagsHash` method (on the Product table) hashes the concatenated tag values via the `ShpfyHash` codeunit. The same pattern is used for `Description Html Hash` and `Image Hash`. + +**Gotcha**: These are non-cryptographic integer hashes. Collisions are theoretically possible. More importantly, the hash only detects changes in the specific fields it covers. If you modify a product field that is not covered by any hash (like Vendor or Product Type), the change detection will not catch it, and the product will not be re-exported unless some hashed field also changes. + +## SystemId linking instead of No./Code + +**Problem**: BC records can be renumbered (Item No. changed). If the connector stored Item No. as the link, renumbering would break the mapping. + +**Solution**: All links from Shopify records to BC records use `SystemId` (Guid) fields. The `Item SystemId` on `Shpfy Product` and `Shpfy Variant` tables links to `Item.SystemId`. The `Customer SystemId` on `Shpfy Customer` links to `Customer.SystemId`. FlowFields like `Shpfy Product."Item No."` resolve through SystemId for display purposes. + +**Example**: In `ShpfyProduct.Table.al`, field 101 `"Item SystemId"` is a Guid. Field 103 `"Item No."` is a FlowField: `CalcFormula = lookup(Item."No." where(SystemId = field("Item SystemId")))`. + +**Gotcha**: The `FindMapping` method in `ShpfyCustomerMapping.Codeunit.al` checks if the BC record still exists by calling `Customer.GetBySystemId`. If the customer was deleted, it clears the SystemId and attempts to re-map. But if `GetBySystemId` fails for other reasons (permissions, filters), the link is silently cleared. + +## Temp table batching + +**Problem**: Processing records one-at-a-time from the API is fragile. Network errors mid-batch leave partial state. The connector needs to separate "retrieve data" from "write to database." + +**Solution**: Import flows retrieve all data into temporary records first, then iterate the temp records to insert/update permanent records. This gives a commit boundary between retrieval and persistence. + +**Example**: In `ShpfySyncProducts.Codeunit.al`, the import flow retrieves all product IDs into a `ProductIds: Dictionary of [BigInteger, DateTime]`, filters against local timestamps, builds a `TempProduct` temp table of products that need updating, then loops over `TempProduct` to run `ProductImport` for each. Each product import runs in its own `Commit()` scope, so one failure does not roll back previous successes. + +**Gotcha**: The `Commit()` call before each `ProductImport.Run()` means that if the import fails, the product record may be in a half-updated state (header committed, lines not yet processed). The error is captured via `GetLastErrorText` but the partial state remains. + +## Data capture for debugging + +**Problem**: When an order import fails or produces unexpected results, you need to see the raw Shopify data that caused it. + +**Solution**: The `Shpfy Data Capture` table (30114) stores raw JSON in a Blob field, linked to any record via `Linked To Table` (integer table ID) and `Linked To Id` (Guid). The `Add` method accepts a table ID, SystemId, and JSON text. + +**Example**: In `ShpfyImportOrder.Codeunit.al`, after inserting each order line, the code calls `DataCapture.Add(Database::"Shpfy Order Line", OrderLine.SystemId, Format(JOrderLine))`. This stores the raw JSON for every order line, making it possible to compare what Shopify sent against what ended up in the BC tables. + +**Gotcha**: Data capture stores the JSON at the time of import. If the Shopify order changes later and is re-imported, the captured data is for the latest import, not the original. There is no history -- each re-import overwrites. + +## Skipped record logging + +**Problem**: Users see "my product didn't sync" and file bugs. The connector needs to explain why it deliberately skipped a record. + +**Solution**: The `ShpfySkippedRecord.Codeunit.al` codeunit provides `LogSkippedRecord` methods that create entries in `Shpfy Skipped Record` (30159) with the Shopify ID, BC record ID, description, and skip reason. + +**Example**: In `ShpfyProductExport.Codeunit.al`, when a variant is blocked: `SkippedRecord.LogSkippedRecord(ItemVariant.RecordId, ItemVariantIsBlockedLbl, Shop)`. The user can open the Skipped Records page to see "Item variant is blocked or sales blocked." + +**Gotcha**: Skipped records accumulate over time. The table has no built-in cleanup. On high-volume shops with many blocked variants, this table can grow large. + +## Negative IDs for BC-created records + +**Problem**: When the connector creates customer addresses in BC and exports them to Shopify, it needs to assign IDs to the address records before Shopify responds with real IDs. But Shopify IDs are positive BigIntegers. + +**Solution**: BC-created addresses use negative IDs. This guarantees no collision with Shopify-assigned positive IDs. When Shopify responds with the real ID, the record can be updated. + +**Example**: `Shpfy Customer Address` records created during customer export get negative `Id` values. The sign convention makes it trivial to filter for BC-created vs. Shopify-created addresses. + +## Cursor-based pagination + +**Problem**: Shopify's GraphQL API uses cursor-based pagination (not offset-based). Each page returns an `endCursor` that must be passed to the next request. + +**Solution**: Query types come in pairs: `GetCustomerIds` / `GetNextCustomerIds`, `GetProductIds` / `GetNextProductIds`, etc. The "Next" variant includes a `{{After}}` parameter for the cursor. The calling code loops: call the first query, extract the cursor from the response, call the "Next" query with the cursor, repeat until `hasNextPage` is false. + +**Example**: The `ShpfyGraphQLType` enum shows 65+ "Next*" entries paired with their initial counterparts. The `ShpfyProductAPI` and other API codeunits implement the pagination loop. + +## Conflict detection via redundancy codes + +**Problem**: An order can be modified in Shopify after it has already been processed into a BC Sales Order. The connector needs to detect this without re-downloading and comparing every field. + +**Solution**: The `Line Items Redundancy Code` on the order header is an integer hash of pipe-separated line IDs. On re-import, the connector computes the hash from the new line data and compares it to the stored value. It also compares total quantity and shipping charges amount. + +**Example**: In `ShpfyImportOrder.Codeunit.al`, `IsImportedOrderConflictingExistingOrder` checks `OrderHeader."Current Total Items Quantity"` against the JSON value, computes the line ID hash, and compares shipping charges. Any mismatch sets `Has Order State Error := true`. + +**Gotcha**: This only detects structural changes (lines added/removed, quantities changed, shipping changed). It does not detect changes to individual line prices, addresses, or metadata. Those changes silently update the Shopify record without flagging a conflict. + +## Legacy patterns to avoid + +### Config template tables (removed v25) + +The Shop table had `Item Template Code` (field 11) and `Customer Template Code` (field 24) of type `Code[10]` referencing `Config. Template Header`. These were replaced by `Item Templ. Code` (field 63) and `Customer Templ. Code` (field 62) referencing BC's native `Item Templ.` and `Customer Templ.` tables. The old fields are `ObsoleteState = Removed` with `ObsoleteTag = '25.0'` and guarded by `#if not CLEANSCHEMA25`. Do not reference the old template fields. + +### Export Customer To Shopify boolean (removed v27) + +Field 29 (`Export Customer To Shopify`) was a boolean that triggered automatic customer export during sync. It was removed in v27 (`ObsoleteTag = '27.0'`) and replaced by a manual "Add to Shopify" action on the Shopify Customers page. The field definition is guarded by `#if not CLEANSCHEMA27`. + +### Log Enabled boolean (removed v26) + +Field 5 (`Log Enabled`) was a simple on/off toggle for logging. It was replaced by the `Logging Mode` enum (field number not adjacent) which supports finer-grained options. The old field is `ObsoleteState = Removed` with `ObsoleteTag = '26.0'` and guarded by `#if not CLEANSCHEMA26`. + +### Owner Resource text field on metafields (removed v28) + +The `Shpfy Metafield` table had an `Owner Resource` text field (field 3) that stored the owner type as a string. This was replaced by the `Owner Type` enum with `IMetafieldOwnerType` interface dispatch. Similarly, the `Value Type` enum field (field 6) was replaced by the `Type` field. Both old fields are `ObsoleteTag = '28.0'`. + +### Tax Code on variant (deprecated v28) + +The `Tax Code` field (field 13) on `Shpfy Variant` was deprecated because Shopify API 2025-10 removed `taxCode` from the `ProductVariant` type. The field is `ObsoleteState = Pending` in v28 and will be removed in v31. diff --git a/src/Apps/W1/Shopify/App/src/Base/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Base/docs/CLAUDE.md new file mode 100644 index 0000000000..6db037fc8f --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Base/docs/CLAUDE.md @@ -0,0 +1,22 @@ +# Base + +Core infrastructure for the Shopify connector. Contains the Shpfy Shop table (ID 30102) -- the central configuration record that nearly every other module reads -- plus the sync orchestration framework, tag storage, communication layer, and role center integration. + +## How it works + +The Shop table is the god object of the connector. It holds 60+ settings covering customer mapping type, product sync direction, stock calculation mode, order processing options, B2B company settings, webhook configuration, currency handling, template references, logging mode, and more. Enabling a shop (`Enabled` field in `ShpfyShop.Table.al`) triggers consent validation via `CustomerConsentMgt.ConfirmUserConsent()` and logs an audit entry. Disabling it first deactivates order-created webhooks and bulk operation webhooks before clearing the Enabled flag. + +`ShpfyBackgroundSyncs.Codeunit.al` is the central dispatcher for all sync operations. Each sync type (customers, companies, products, inventory, orders, payouts, etc.) follows the same pattern: build XML parameters embedding the Shop record view, then call `EnqueueJobEntry` which either schedules a Job Queue Entry (if `Allow Background Syncs` is true and `TaskScheduler.CanCreateTask()` succeeds) or runs the report synchronously. The codeunit splits shops into two passes -- background-capable and foreground-only -- so a single call handles mixed configurations. + +`ShpfySynchronizationInfo.Table.al` tracks the last successful sync timestamp per shop and sync type (Products, Orders, Customers, Companies), keyed by `[Shop Code, Synchronization Type]`. Each sync codeunit records its start time and writes it back after completion. + +Tags (`ShpfyTag.Table.al`) are stored as normalized rows with a `[Parent Id, Tag]` composite key. The `OnInsert` trigger enforces Shopify's 250-tag-per-entity limit. `UpdateTags` does a full replace -- it deletes all existing tags for the parent, then re-inserts from a comma-separated string. + +## Things to know + +- `ShpfyCommunicationEvents.Codeunit.al` publishes internal events for every HTTP interaction (OnClientSend, OnClientPost, OnClientGet, OnGetAccessToken, OnGetContent). These are the hooks test codeunits use to mock the Shopify API. +- `ShpfyShopMgt.Codeunit.al` manages notification lifecycle for API version expiration and blocking, not shop CRUD. The actual shop lifecycle is mostly handled by the Shop table's field triggers. +- The `ShpfyInitialImport` page and codeunit provide a guided first-time sync experience, and `ShpfyBackgroundSyncs` subscribes to `Job Queue Entry.OnBeforeModifyEvent` to track initial import progress. +- Page extensions on Business Manager RC, Order Processor RC, and Sales Rel Mgr RC inject the Shopify Activities cue group, driven by `ShpfyCue.Table.al`. +- The `ShpfyFilterMgt.Codeunit.al` handles filter serialization for passing record views through job queue XML parameters. +- `ShpfyUpgradeMgt.Codeunit.al` handles data migrations across versions; `ShpfyInstaller.Codeunit.al` runs on first install. diff --git a/src/Apps/W1/Shopify/App/src/Catalogs/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Catalogs/docs/CLAUDE.md new file mode 100644 index 0000000000..8681e36edf --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Catalogs/docs/CLAUDE.md @@ -0,0 +1,19 @@ +# Catalogs + +B2B catalog pricing contexts -- not product groupings. Each catalog defines a pricing configuration (Customer Price Group, Customer Discount Group, posting groups, currency, VAT settings) that determines how BC calculates prices for a Shopify B2B company or market. + +## How it works + +`ShpfyCatalog.Table.al` is the central record, keyed by (Id, Company SystemId). It holds a full pricing context: `Customer Price Group`, `Customer Discount Group`, `Gen. Bus. Posting Group`, `VAT Bus. Posting Group`, `Tax Area Code`, `Customer Posting Group`, plus `Prices Including VAT`, `Allow Line Disc.`, and `Currency Code`. When a `Customer No.` is assigned, its discount and price group settings take precedence over the catalog-level settings. + +`ShpfySyncCatalogPrices.Codeunit.al` drives the price sync. It iterates all catalogs with `Sync Prices = true`, retrieves existing Shopify prices into a temporary `Shpfy Catalog Price` table, recalculates each variant's price from BC using `Shpfy Product Price Calc.`, and pushes updates back via GraphQL in batches of 250 variants. The `Shpfy Catalog Price` table is `TableType = Temporary` -- it exists only during calculation and is never persisted. + +The `Shpfy Market Catalog Relation` table links catalogs to Shopify markets, enabling multi-market pricing where different markets can have different catalog configurations. + +## Things to know + +- The catalog primary key includes `Company SystemId`, meaning the same Shopify catalog Id can appear multiple times if it's associated with different companies (though in practice this reflects B2B company-catalog relationships). +- `Shpfy Catalog Price` is explicitly `TableType = Temporary`. If you see code reading from it, it was populated earlier in the same process -- there is no persistent price cache. +- The `Catalog Type` enum (`ShpfyCatalogType.Enum.al`) distinguishes catalog kinds and can be used to filter sync operations via `SetCatalogType`. +- Price sync respects `Item.Blocked` and `Item."Sales Blocked"` -- blocked items are silently skipped, not errored. +- `ShpfySyncCatalogs.Report.al` and `ShpfySyncCatalogPrices.Report.al` are the user-facing entry points, following the connector's report-as-batch-job pattern. diff --git a/src/Apps/W1/Shopify/App/src/Companies/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Companies/docs/CLAUDE.md new file mode 100644 index 0000000000..feabb7558f --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Companies/docs/CLAUDE.md @@ -0,0 +1,20 @@ +# Companies + +B2B company management for Shopify Plus stores. Companies are a separate entity model from customers in Shopify -- a company has one or more locations, each of which can map independently to BC customers. This module mirrors the customer sync pattern but adds the location dimension and tax registration matching. + +## How it works + +`ShpfySyncCompanies.Codeunit.al` follows the same bidirectional structure as customer sync. Import mode is controlled by `Company Import From Shopify` on the Shop (AllCompanies or WithOrderImport). Like customer sync, it retrieves a `Dictionary of [BigInteger, DateTime]` of company IDs, skips unchanged records by comparing `Updated At` vs `Last Updated by BC`, and feeds each into `ShpfyCompanyImport`. For export, `ShpfyCompanyExport` runs against BC Customer records. + +The Shpfy Company table (ID 30150) links to a BC Customer via `Customer SystemId` and tracks its main contact through `Main Contact Customer Id` (the Shopify customer BigInteger) and `Main Contact Id`. Each company can have multiple locations stored in `ShpfyCompanyLocation.Table.al` (ID 30151), linked back to the company via `Company SystemId` (a Guid, not the Shopify BigInteger). Locations carry their own `Sell-to Customer No.` and `Bill-to Customer No.`, enabling org-based order routing where different locations map to different BC customers. + +Mapping uses the `Shpfy ICompany Mapping` interface, selected by `Company Mapping Type` on the Shop. The `ByTaxId` strategy (`ShpfyCompByTaxId.Codeunit.al`) looks up a company location's `Tax Registration Id` against BC customers using a pluggable `Shpfy Tax Registration Id Mapping` interface -- the shop's `Shpfy Comp. Tax Id Mapping` field selects between VAT Registration No. and Tax Registration No. matchers. `CompanyMapping.FindMapping` has a fallback: if the selected strategy does not implement `IFindCompanyMapping`, it silently falls back to `CompByEmailPhone`. + +## Things to know + +- Company locations have a `Shpfy Payment Terms Id` that links to a Shopify payment terms table, enabling per-location payment terms mapping. +- The `Bill-to Customer No.` field on a location requires `Sell-to Customer No.` to be set first (enforced by a `TestField` in the validate trigger). Clearing sell-to also clears bill-to. +- Two reports provide manual push actions: `AddCompanyToShopify` and `AddCustasLocations` (the latter adds BC customers as locations under an existing company). +- Deleting a company record cascades to delete its locations via the `OnDelete` trigger. +- Unlike the Customer table which stores `Shop Id` as an Integer, the Company table also stores `Shop Code` as a Code[20] with a table relation to `Shpfy Shop` -- a structural inconsistency between the two modules. +- The `CompanyImportRange` enum has the same values as `CustomerImportRange` (None, WithOrderImport, AllCompanies) but is a separate enum. diff --git a/src/Apps/W1/Shopify/App/src/Customers/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Customers/docs/CLAUDE.md new file mode 100644 index 0000000000..bb68dfc2f4 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Customers/docs/CLAUDE.md @@ -0,0 +1,20 @@ +# Customers + +Bidirectional sync between Shopify customers and BC customers. The Shpfy Customer table (ID 30105) links to BC via a `Customer SystemId` Guid. Customer addresses live in a separate table keyed by a BigInteger `Id` that uses negative values (-1, -2, ...) for BC-created addresses not yet synced to Shopify, assigned in the `OnInsert` trigger of `ShpfyCustomerAddress.Table.al`. + +## How it works + +`ShpfySyncCustomers.Codeunit.al` orchestrates both directions. Import mode is controlled by the Shop's `Customer Import From Shopify` setting -- `AllCustomers` pulls everyone, `WithOrderImport` only refreshes already-known customers when the shop also has `Shopify Can Update Customer` enabled. In both cases the codeunit retrieves a `Dictionary of [BigInteger, DateTime]` of Shopify customer IDs, compares `Updated At` and `Last Updated by BC` timestamps to skip unchanged records, then feeds each changed customer into `ShpfyCustomerImport` inside a Commit/ClearLastError loop so one failure does not abort the batch. + +For the BC-to-Shopify direction, `ShpfyCustomerExport.Codeunit.al` iterates BC customers, calls `ShpfyCustomerMapping` to find or create the Shopify counterpart, then pushes updates through `ShpfyCustomerAPI`. Creating a customer in Shopify requires a non-empty email -- records without one are logged as skipped. The export also syncs metafields if `Customer Metafields To Shopify` is enabled on the shop. + +Mapping strategy is selected by the Shop's `Customer Mapping Type` enum, dispatched through the `Shpfy ICustomer Mapping` interface. If both Name fields are blank, `ShpfyCustomerMapping.DoMapping` forces the `By EMail/Phone` strategy regardless of the shop setting. The `ByEmailPhone` strategy in `ShpfyCustomerMapping.DoFindMapping` uses a case-insensitive email filter (`'@' + Email`) and a fuzzy phone filter -- `CreatePhoneFilter` strips all non-digits, trims leading zeros, then inserts `*` wildcards between every digit to match any formatting. + +## Things to know + +- Customer state in Shopify (Disabled, Invited, Enabled, Declined in `ShpfyCustomerState.Enum.al`) is unrelated to BC's Blocked field -- there is no automatic mapping between them. +- Name assembly for export is configurable per shop via `Name Source`, `Name 2 Source`, and `Contact Source` fields, each selecting a strategy like CompanyName, FirstAndLastName, or LastAndFirstName. +- Multi-email addresses in BC (semicolon or comma separated) are handled by export -- only the first email is sent to Shopify. +- `ShpfyCustomerEvents.Codeunit.al` publishes integration events at every stage: before/after find mapping, before/after create/update customer, and before/after find customer template. These are the primary extension points for partner customizations. +- County handling on export requires a matching `Shpfy Tax Area` row. If the shop's `County Source` is Code and the county string exceeds the field length, export raises a hard error rather than truncating silently. +- The `AddCustomerToShopify` report provides the manual "push" action that replaced the removed auto-export behavior. diff --git a/src/Apps/W1/Shopify/App/src/Document Links/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Document Links/docs/CLAUDE.md new file mode 100644 index 0000000000..b100440d67 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Document Links/docs/CLAUDE.md @@ -0,0 +1,17 @@ +# Document Links + +Bidirectional navigation between Shopify documents (orders, returns, refunds) and BC documents (sales orders, invoices, credit memos, shipments, return receipts). The junction table `Shpfy Doc. Link To Doc.` creates an N:M relationship that survives the BC document lifecycle from draft through posting. + +## How it works + +`ShpfyDocumentLinkMgt.Codeunit.al` subscribes to BC sales posting events to propagate links as documents move through their lifecycle. When a Sales Order is posted, the codeunit hooks into `OnAfterPostSalesDoc` and creates new link records for the resulting Posted Sales Shipment, Posted Sales Invoice, Posted Return Receipt, and Posted Sales Credit Memo. It also handles the `OnBeforeDeleteAfterPosting` event to create links before BC deletes the original sales header. For standalone invoices that reference shipments, it traces back through `Sales Invoice Line."Shipment No."` to find the originating Shopify document. + +The two enums `ShpfyDocumentType.Enum.al` and `ShpfyShopDocumentType.Enum.al` implement `IOpenBCDocument` and `IOpenShopifyDocument` respectively, enabling polymorphic document opening. Each enum value maps to a dedicated codeunit (like `Shpfy Open SalesOrder` or `Shpfy Open Refund`) that opens the correct page. Unrecognized types fall through to `Shpfy OpenBCDoc NotSupported` or `Shpfy OpenDoc NotSupported`. + +## Things to know + +- The table has a four-part composite key: (Shopify Document Type, Shopify Document Id, Document Type, Document No.) with secondary indexes for lookups from either side. +- Link creation is guarded -- `CreateNewDocumentLink` silently skips when any key field is blank or zero, so partial posting scenarios don't produce orphaned links. +- Both enums are `Extensible = true`, so partners can add new Shopify or BC document types without modifying base code. +- The `OpenShopifyDocument` and `OpenBCDocument` methods on the table itself resolve the interface from the enum value and delegate, keeping the page layer clean of conditional logic. +- When a Sales Header is deleted directly (not via posting), the `OnDeleteSalesHeader` subscriber cleans up associated link records. diff --git a/src/Apps/W1/Shopify/App/src/Inventory/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Inventory/docs/CLAUDE.md new file mode 100644 index 0000000000..5182877bcc --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Inventory/docs/CLAUDE.md @@ -0,0 +1,20 @@ +# Inventory + +Stock level sync from BC to Shopify. The flow is primarily export-oriented -- BC calculates stock per item/variant/location and pushes it to Shopify. Import of current Shopify stock levels happens first to enable delta calculations, but can be skipped via `SetSkipImport`. + +## How it works + +`ShpfySyncInventory.Codeunit.al` (TableNo = "Shpfy Shop Inventory") runs in two phases. First, unless `SkipImport` is set, it iterates all `Shpfy Shop Location` records where `Stock Calculation` is not Disabled, calling `InventoryAPI.ImportStock` for each to pull current Shopify quantities into `Shpfy Shop Inventory`. Then it calls `InventoryAPI.ExportStock` to push BC-calculated stock. + +The actual stock calculation lives in `ShpfyInventoryAPI.GetStock`. For each `Shpfy Shop Inventory` row, it resolves the linked Item via the variant's `Item SystemId`, applies the location's `Location Filter` and variant filter, then delegates to the `Shpfy Stock Calculation` interface. The enum (`ShpfyStockCalculation.Enum.al`) provides two built-in strategies -- "Projected Available Balance at Today" (`ShpfyBalanceToday`) and "Free Inventory (not reserved)" (`ShpfyFreeInventory`). The enum is `Extensible = true` and implements two interfaces simultaneously: `Shpfy Stock Calculation` for the basic `GetStock(Item)` signature and `Shpfy IStock Available` which controls whether a location reports as stock-capable at all. There is also `Shpfy Extended Stock Calculation`, an interface that extends the base with a `GetStock(Item, ShopLocation)` overload -- `GetStock` checks `is "Shpfy Extended Stock Calculation"` at runtime to pick the right call. + +After the strategy returns a value, `GetStock` adjusts for unit of measure (if the variant maps to a UoM option), then fires `OnAfterCalculationStock` from `ShpfyInventoryEvents.Codeunit.al` to let subscribers apply custom adjustments. + +## Things to know + +- Each Shopify location maps to one `Shpfy Shop Location` row, which carries a `Location Filter` (a text filter like "MAIN|WEST") for aggregating stock across multiple BC locations, and a `Default Location Code` used on sales documents. Setting one auto-populates the other if blank. +- `Default Product Location` on a shop location cannot mix standard locations with fulfillment service locations -- the validate trigger on `ShpfyShopLocation.Table.al` blocks this with a client error. +- `Is Fulfillment Service` distinguishes merchant warehouses from 3PL providers; fulfillment service locations carry their own callback URL and service ID. +- The `Shpfy Shop Inventory` table is keyed by `[Shop Code, Product Id, Variant Id, Location Id]` and tracks both `Shopify Stock` (last imported) and `Stock` (last calculated from BC), each with its own timestamp. +- `Disabled` is the `InitValue` for `Stock Calculation` on new locations -- stock sync is opt-in per location, not automatic. +- The `SyncShopLocations` codeunit pulls the list of locations from Shopify; it runs separately from the inventory sync itself. diff --git a/src/Apps/W1/Shopify/App/src/Logs/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Logs/docs/CLAUDE.md new file mode 100644 index 0000000000..77332ce9a9 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Logs/docs/CLAUDE.md @@ -0,0 +1,19 @@ +# Logs + +Three logging mechanisms: `Shpfy Log Entry` for API request/response recording, `Shpfy Data Capture` for raw JSON snapshots linked to any entity, and `Shpfy Skipped Record` for records that couldn't be imported. These serve different diagnostic purposes and are referenced throughout the connector. + +## How it works + +`Shpfy Log Entry` stores individual API calls with request/response blobs, HTTP status codes, method, URL, retry count, query cost, and a Shopify request Id for correlation. `ShpfyLogEntries.Codeunit.al` adds escalation support -- entries with status code 500 that are less than 14 days old can generate a downloadable escalation report containing the request Id, timestamp, store URL, API version, and full request/response payloads for submission to Shopify support. + +`Shpfy Data Capture` is a generic JSON snapshot store. Its `Add` method accepts a table number, a SystemId, and JSON data. It computes a hash via `Shpfy Hash` and skips writing if the latest capture for that record already has the same hash, avoiding duplicate snapshots. Nearly every import operation across the connector calls `DataCapture.Add` after writing a record. + +`Shpfy Skipped Record` logs records that failed to import with a `Shopify Id`, the table/record that was being processed, and a `Skipped Reason`. The `ShowPage` method uses `Page Management` to navigate directly to the related record from the skipped records list. + +## Things to know + +- Data Capture links to entities via (Linked To Table, Linked To Id) where `Linked To Id` is the SystemId (Guid), not the primary key. This means if a record is deleted, its data captures become orphaned unless the parent table's OnDelete trigger cleans them up (most Shopify tables do this). +- The hash-based deduplication in `DataCapture.Add` means consecutive identical API responses for the same record produce only one capture entry, keeping storage manageable. +- Log entry deletion is age-based via `DeleteEntries(DaysOld)` on both `ShpfyLogEntry` and `ShpfySkippedRecord`, with a confirmation dialog. +- The escalation report feature in `ShpfyLogEntries.Codeunit.al` first tests the Shopify connection to verify it's a server-side issue, then warns the user to review data for sensitive information before downloading. +- Skipped records store a `Record ID` (RecordID type) that enables the `ShowPage` drill-down, but the description is computed from primary key filters on the referenced record, with special handling for `Shpfy Catalog` which uses the Name field instead. diff --git a/src/Apps/W1/Shopify/App/src/Metafields/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Metafields/docs/CLAUDE.md new file mode 100644 index 0000000000..b8fe45d4af --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Metafields/docs/CLAUDE.md @@ -0,0 +1,20 @@ +# Metafields + +Shopify's custom field system, implemented as typed key-value pairs that attach to products, variants, customers, or companies. This module handles the full lifecycle: type validation, value editing, and two-way sync via GraphQL. + +## How it works + +The Shpfy Metafield table (ID 30101) stores each metafield with a `Namespace` + `Name` composite identity (e.g., "custom.warranty_years"). The `Owner Type` enum (Customer, Product, ProductVariant, Company) determines which parent entity a metafield belongs to, and `Parent Table No.` is auto-calculated from it via the `IMetafieldOwnerType` interface -- each owner type implementation returns the correct BC table ID and can retrieve the shop code from the owner. + +Type safety is enforced through the `Shpfy IMetafield Type` interface. Each Shopify type (single_line_text_field, number_integer, number_decimal, money, date, boolean, url, color, weight, dimension, volume, etc.) has its own codeunit in the `IMetafieldType` subfolder implementing `IsValidValue`, `GetExampleValue`, `HasAssistEdit`, and `AssistEdit`. When a Value is validated on the table, the current Type's `IsValidValue` is called; on failure the error message includes the example value. The `money` type (`ShpfyMtfldTypeMoney.Codeunit.al`) additionally validates that the currency code in the JSON `{"amount":"5.99","currency_code":"CAD"}` matches the shop's configured currency (falling back to LCY from General Ledger Setup). + +The public API codeunit `ShpfyMetafields.Codeunit.al` (ID 30418, Access = Public) exposes three procedures: `GetMetafieldDefinitions`, `SyncMetafieldToShopify`, and `SyncMetafieldsToShopify`. The bulk sync only sends metafields whose `Last Updated by BC` timestamp indicates they changed since the last sync. + +## Things to know + +- New metafields created in BC get negative IDs (assigned in `OnInsert`: first gets -1, subsequent get `min existing - 1`), just like customer addresses. These are replaced with real Shopify IDs after the first sync. +- The `OnModify` trigger automatically stamps `Last Updated by BC` to CurrentDateTime, which prevents sync loops -- the import side checks this timestamp against Shopify's `Updated At`. +- Default namespace for BC-created metafields is `Microsoft.Dynamics365.BusinessCentral`. +- The deprecated `string` and `integer` types are blocked in the `Type` validate trigger with explicit error messages directing users to `single_line_text_field` and `number_integer` respectively. +- `Owner Resource` (the old text-based owner field) was removed in v28 -- only `Owner Type` (the enum) remains. +- The `money` type's `TryExtractValues` is a `[TryFunction]` that also validates the currency code exists in the BC Currency table and ensures the JSON has exactly 2 keys -- extra fields fail validation silently. diff --git a/src/Apps/W1/Shopify/App/src/Order Fulfillments/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Order Fulfillments/docs/CLAUDE.md new file mode 100644 index 0000000000..acb4c23993 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Order Fulfillments/docs/CLAUDE.md @@ -0,0 +1,18 @@ +# Order Fulfillments + +Two distinct but related concepts live here: fulfillment orders (Shopify's plan for what should ship from where) and order fulfillments (actual shipments with tracking). A single Shopify order can have multiple fulfillment orders when items are allocated to different locations, and each fulfillment order can produce multiple fulfillments for partial shipping. + +## How it works + +`ShpfyFulfillmentOrdersAPI.Codeunit.al` manages the fulfillment order lifecycle. It fetches fulfillment orders from Shopify via GraphQL, creates local `Shpfy FulFillment Order Header` and `Shpfy FulFillment Order Line` records, and handles the fulfillment service registration that BC needs to accept and process fulfillment requests. The API supports accepting pending fulfillment requests (SUBMITTED status) before creating actual fulfillments. + +`ShpfyExportShipments.Codeunit.al` drives the outbound flow -- when a BC Sales Shipment is posted, it builds a GraphQL `fulfillmentCreate` mutation. The logic matches shipment lines to open fulfillment order lines by `Line Item Id`, distributing quantities across fulfillment orders when needed. It batches up to 250 line items per mutation. If fulfillment order lines are assigned to a third-party fulfillment service (not the BC service), they are skipped. Tracking info from the BC Shipping Agent maps to Shopify's tracking company via the `Shpfy Tracking Companies` enum. + +`ShpfyOrderFulfillments.Codeunit.al` handles the inbound side, importing actual fulfillment records with tracking numbers, URLs, and fulfillment line items, including gift card detection. + +## Things to know + +- Fulfillment Order Lines track `Remaining Quantity`, `Total Quantity`, and `Quantity to Fulfill` -- the last one is a working field used during export to accumulate how much to include in the current mutation. +- `ShpfyExportShipments` sets `Shpfy Fulfillment Id` to -1 on the Sales Shipment Header when no matching fulfillment lines are found or when Shopify rejects the request, preventing repeated attempts. +- The fulfillment service is auto-registered on first outbound request if `Allow Outgoing Requests` is enabled but `Fulfillment Service Activated` is false. +- Fulfillment Order Header carries `Request Status` (UNSUBMITTED, SUBMITTED, ACCEPTED, etc.) which gates whether BC can create fulfillments -- assigned orders with SUBMITTED status must be accepted first via `AcceptFulfillmentRequest`. diff --git a/src/Apps/W1/Shopify/App/src/Order Refunds/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Order Refunds/docs/CLAUDE.md new file mode 100644 index 0000000000..e5f476bcae --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Order Refunds/docs/CLAUDE.md @@ -0,0 +1,17 @@ +# Order Refunds + +Shopify refund data -- the money going back to the customer. Refunds are separate from returns (physical goods); a refund can exist without a return (appeasement refund) or be linked to one via `Return Id`. The actual BC credit memo creation lives in `Order Return Refund Processing` -- this area just holds the data model and API import logic. + +## How it works + +`ShpfyRefundsAPI.Codeunit.al` imports refund headers, refund lines, and refund shipping lines from Shopify. The `Shpfy Refund Header` carries dual-currency totals (`Total Refunded Amount` and `Pres. Tot. Refunded Amount`), an optional `Return Id` link, and error tracking via blob fields (`Last Error Description`, `Last Error Call Stack`). The `Is Processed` FlowField checks the `Shpfy Doc. Link To Doc.` table for a matching refund document link, which is how the system knows whether a BC credit memo already exists. + +Refund lines (`ShpfyRefundLine.Table.al`) carry per-line amounts at several granularities: `Amount` (unit price), `Subtotal Amount` (quantity times price after discounts), and `Total Tax Amount`, all in both shop and presentment currencies. The `Restock Type` enum (`no_restock`, `cancel`, `return`, `legacy_restock`) controls how `Order Return Refund Processing` creates the credit memo line -- this is one of the most important fields for understanding refund behavior. Refund shipping lines (`ShpfyRefundShippingLine.Table.al`) are separate records that capture shipping cost refunds with their own subtotal and tax amounts. + +## Things to know + +- `Has Processing Error` is a regular boolean field (not a FlowField) that gets set to true/false inside `SetLastErrorDescription` based on whether the description text is non-empty. +- The `Can Create Credit Memo` boolean on refund lines gates processing -- when false, `ShpfyRetRefProcCrMemo` skips the entire refund. This field is set during import based on Shopify data. +- Many FlowFields on the header (`Sell-to Customer No.`, `Bill-to Customer Name`, `Shopify Order No.`, `Return No.`) resolve through the parent order or linked return, so querying these requires the related records to exist. +- The header's `CheckCanCreateDocument` method queries the document link table directly, providing a public-facing check independent of the `Is Processed` FlowField. +- The `Note` field is a blob with Get/Set procedures, consistent with the pattern used across returns and other areas for storing variable-length text from Shopify. diff --git a/src/Apps/W1/Shopify/App/src/Order Return Refund Processing/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Order Return Refund Processing/docs/CLAUDE.md new file mode 100644 index 0000000000..1a07764099 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Order Return Refund Processing/docs/CLAUDE.md @@ -0,0 +1,17 @@ +# Order Return Refund Processing + +The processing engine that converts Shopify refunds into BC sales credit memos. This area owns the strategy pattern and the actual credit memo creation logic, while the data models for returns and refunds live in their own sibling folders. + +## How it works + +The `Shpfy ReturnRefund ProcessType` enum implements `Shpfy IReturnRefund Process` with three strategies: blank (default, does nothing), "Import Only" (imports Shopify data but creates no BC documents), and "Auto Create Credit Memo" (the main workhorse in `ShpfyRetRefProcCrMemo.Codeunit.al`). The credit memo strategy validates prerequisites -- the parent order must already be processed in BC, and the refund must not already have a document link -- before delegating to `ShpfyCreateSalesDocRefund.Codeunit.al`. + +`ShpfyCreateSalesDocRefund` builds the credit memo in several phases: create the header (copying addresses and customer data from the original order header), create item lines from refund lines (respecting restock type), fall back to return lines if no refund lines exist, add refund shipping lines, and finally balance the total with a remaining-amount line posted to the Shop's "Refund Account". The restock type on each line determines how it is handled -- `Return` and `Legacy Restock` create item-type lines, `No Restock` uses the "Refund Acc. non-restock Items" G/L account, and `Cancel` uses the "Refund Account" G/L account. + +## Things to know + +- The `Shpfy IDocument Source` interface exists solely for error reporting back to the source record. `ShpfyIDocSourceRefund.Codeunit.al` writes error details (including call stack via `Shpfy Extended IDocument Source`) to the Refund Header's blob fields. +- Refund processing runs inside `if CreateSalesDocRefund.Run()` with a `Commit()` before and after, isolating failures so one bad refund doesn't roll back others. +- The credit memo is automatically released via `ReleaseSalesDocument.Run` after creation -- it arrives in BC ready for posting. +- Currency handling branches on `Processed Currency Handling` (Shop Currency vs Presentment Currency) throughout line creation, which means unit prices come from different refund line fields depending on the shop setting. +- Table extensions on `Sales Cr.Memo Header`, `Sales Cr.Memo Line`, `Return Receipt Header`, and `Return Receipt Line` add Shopify tracking fields to posted BC documents. diff --git a/src/Apps/W1/Shopify/App/src/Order Returns/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Order Returns/docs/CLAUDE.md new file mode 100644 index 0000000000..0fee88b794 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Order Returns/docs/CLAUDE.md @@ -0,0 +1,17 @@ +# Order Returns + +Shopify return records -- physical goods coming back from the customer. Returns are separate from refunds; a return tracks what items are being sent back, while a refund (in `Order Refunds`) tracks the money going out. A refund may optionally link to a return, but they can exist independently. + +## How it works + +`ShpfyReturnsAPI.Codeunit.al` fetches returns from Shopify and populates `Shpfy Return Header` and `Shpfy Return Line`. The header carries the return status, total quantity, and optional decline information (reason enum plus a blob-stored decline note). Return lines link back to order lines via `Order Line Id` and to fulfillment lines via `Fulfillment Line Id`, establishing the chain from what was shipped to what is coming back. + +Each return line has `Refundable Quantity` and `Refunded Quantity` fields, tracking how much of the returned goods have been financially resolved. The `Type` field distinguishes between regular returns and exchanges via the `Shpfy Return Line Type` enum. Return reason details are stored in three fields: the deprecated `Return Reason` enum, plus the newer `Return Reason Name` and `Return Reason Handle` text fields that align with Shopify's customizable return reasons. + +## Things to know + +- Return lines carry dual-currency `Discounted Total Amount` and `Presentment Disc. Total Amt.` with a SumIndexField key for efficient aggregation at the header level via FlowField. +- The `Decline Note` and `Return Reason Note` fields are blobs with explicit Get/Set procedures that handle InStream/OutStream -- they are not simple text fields. +- The `Return Reason` enum field (field 6) is marked as deprecated in its caption ("Return Reason (deprecated)"). Use `Return Reason Name` and `Return Reason Handle` instead. +- Return lines include `Location Id` for identifying which Shopify location the goods are returning to, used by `Order Return Refund Processing` when determining the BC location code on credit memo lines. +- `ShpfyReturnEnumConvertor.Codeunit.al` handles the conversion of Shopify API status and reason strings to AL enum values. diff --git a/src/Apps/W1/Shopify/App/src/Order handling/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Order handling/docs/CLAUDE.md new file mode 100644 index 0000000000..3fb2394282 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Order handling/docs/CLAUDE.md @@ -0,0 +1,19 @@ +# Order handling + +This module imports Shopify orders via GraphQL and converts them into BC Sales Orders or Sales Invoices. It is the most complex business domain in the connector -- an import touches header parsing, paginated line retrieval, fulfillment orders, tax lines, shipping charges, transactions, returns, refunds, customer/company mapping, and conflict detection, all before a BC document is created. + +## How it works + +The entry point is `ShpfySyncOrdersfromShopify.Report.al`, which calls `ShpfyOrdersAPI` to retrieve a lightweight list of candidate orders into the `Shpfy Orders to Import` staging table (30121). Each row in that staging table is then processed by `ShpfyImportOrder.Codeunit.al`, which fetches the full order via GraphQL, populates the `Shpfy Order Header` (30118) and `Shpfy Order Line` (30119) records, and pulls in related data -- tax lines, shipping charges, fulfillment orders, transactions, returns, and refunds. When an already-processed order is re-imported, `IsImportedOrderConflictingExistingOrder` compares three things: the current items quantity, a hash of pipe-separated line IDs ("Line Items Redundancy Code"), and shipping charges amount. A mismatch sets `Has Order State Error` and blocks further processing until resolved. + +The second phase runs through `ShpfyCreateSalesOrders.Report.al`, which delegates to `ShpfyProcessOrders` and ultimately `ShpfyProcessOrder.Codeunit.al`. This codeunit first calls `ShpfyOrderMapping` to resolve customer/company and line-item mappings, then builds a Sales Header and Sales Lines. If `Fulfillment Status = Fulfilled` and the shop has `Create Invoices From Orders` enabled, an Invoice is created instead of an Order. Tip lines route to the shop's Tip Account G/L, gift card lines to the Sold Gift Card Account, and shipping charges become either a G/L line or an item charge depending on the `Shpfy Shipment Method Mapping` configuration. After lines are created, global discounts (the portion of order discount not already allocated to individual lines or shipping) are applied via `SalesCalcDiscountByType`. If `Auto Release Sales Orders` is on, the document is released automatically. + +## Things to know + +- Amounts exist in two currencies: shop currency (`Currency Code`, `Total Amount`) and presentment currency (`Presentment Currency Code`, `Presentment Total Amount`). The shop's `Currency Handling` setting controls which set flows into BC sales documents -- see the `case ShopifyShop."Currency Handling"` blocks in `ShpfyProcessOrder`. +- `Location Id` on order lines comes from fulfillment order lines (`ShpfyFulFillmentOrderLine`), not directly from the Shopify order line JSON. See `UpdateLocationIdAndDeliveryMethodOnOrderLine` in `ShpfyImportOrder`. +- Refund quantities are subtracted from order line quantities in `ConsiderRefundsInQuantityAndAmounts`. Lines that reach zero quantity are then deleted. A refund line with `Can Create Credit Memo = false` on a processed order triggers a conflict error. +- B2B orders (`B2B = true`) take a separate mapping path through `MapB2BHeaderFields` in `ShpfyOrderMapping`, which resolves company and company location to sell-to/bill-to customers. +- The `Use Shopify Order No.` flag lets a specific order use the Shopify order number as the BC document number, but validation in `ShpfyProcessOrder` rejects any order number starting with `@`. +- Order line pagination is handled inside `RetrieveAndSetOrderLines` -- it loops on `hasNextPage` / `endCursor` from the GraphQL response, so orders with many lines are not truncated. +- Conflict detection is deliberately conservative: once `Has Order State Error` is set, re-import will not clear it. Manual resolution or calling `MarkOrderConflictAsResolved` is required. diff --git a/src/Apps/W1/Shopify/App/src/Order handling/docs/data-model.md b/src/Apps/W1/Shopify/App/src/Order handling/docs/data-model.md new file mode 100644 index 0000000000..cef2c1e979 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Order handling/docs/data-model.md @@ -0,0 +1,54 @@ +# Data model + +## Overview + +The order handling data model has two conceptual areas: the Shopify-side staging tables that capture the raw imported order, and the BC-side table extensions that stamp Shopify identifiers onto standard Sales documents. The staging tables are keyed by Shopify's BigInteger IDs and are intentionally denormalized -- the header carries three full address sets and dual-currency totals because Shopify delivers them in a single GraphQL response and the connector wants to avoid follow-up calls. + +## Shopify order staging + +```mermaid +erDiagram + ORDERS_TO_IMPORT ||--o| ORDER_HEADER : "imported into" + ORDER_HEADER ||--o{ ORDER_LINE : "contains" + ORDER_HEADER ||--o{ ORDER_TAX_LINE : "taxed by" + ORDER_HEADER ||--o{ ORDER_ATTRIBUTE : "annotated with" + ORDER_LINE ||--o{ ORDER_TAX_LINE : "taxed by" + + ORDERS_TO_IMPORT { + } + ORDER_HEADER { + } + ORDER_LINE { + } + ORDER_TAX_LINE { + } + ORDER_ATTRIBUTE { + } +``` + +`Shpfy Orders to Import` (30121) is a transient staging table populated by the sync report. It holds lightweight metadata (financial status, fulfillment status, order amount, tags) used to decide whether an order should be imported. Once imported, the full order lives in `Shpfy Order Header` (30118) and `Shpfy Order Line` (30119). + +`Shpfy Order Tax Line` (30122) is polymorphic in its parent key: `Parent Id` can reference either an Order Header (the header's `Shopify Order Id`) or an Order Line (the line's `Line Id`). The table resolves currency codes by walking from the line to its parent header in `OrderCurrencyCode()`. This parent ambiguity means you cannot read tax lines without knowing whether the parent is a header or a line. + +`Shpfy Order Attribute` (30116) stores custom checkout key-value pairs keyed by `(Order Id, Key)`. The value field was widened from 250 to 2048 characters in version 27 -- old code referencing the removed `Value` field will not compile. + +The discount application table `Shpfy Order Disc.Appl.` (30117) captures Shopify's discount allocation metadata (allocation method, target type, value type) but is not directly consumed during BC document creation. Instead, line-level discount amounts are computed from `discountAllocations` in the GraphQL JSON, and any remaining "global" discount is applied as an invoice discount after all lines are created. + +## BC document extensions + +```mermaid +erDiagram + SALES_HEADER ||--o{ SALES_LINE : "contains" + ORDER_HEADER ||--o| SALES_HEADER : "creates" + + SALES_HEADER { + } + SALES_LINE { + } + ORDER_HEADER { + } +``` + +The `Shpfy Sales Header` table extension (30101) adds `Shpfy Order Id` (BigInteger), `Shpfy Order No.` (Code[50]), and `Shpfy Refund Id` to the standard Sales Header. The `Shpfy Sales Line` extension (30104) adds `Shpfy Order Line Id`, `Shpfy Order No.`, `Shpfy Refund Id`, `Shpfy Refund Line Id`, and `Shpfy Refund Shipping Line Id`. These fields survive posting -- archive table extensions (`ShpfySalesHeaderArchive`, `ShpfySalesLineArchive`) and posted invoice extensions carry the same identifiers, ensuring traceability back to the Shopify order throughout the document lifecycle. + +The link between the Shopify order and the BC document is also recorded in `Shpfy Doc. Link To Doc.`, a separate junction table. The `IsProcessed()` method on Order Header checks both the `Processed` flag and this link table, so an order is considered processed if either condition is true. diff --git a/src/Apps/W1/Shopify/App/src/Payments/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Payments/docs/CLAUDE.md new file mode 100644 index 0000000000..30a2874a75 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Payments/docs/CLAUDE.md @@ -0,0 +1,17 @@ +# Payments + +Shopify Payments account data -- payouts, payment transactions, disputes, and payment terms. This is about the money moving between Shopify and the merchant's bank, not order-level transactions (those live in `Transactions`). + +## How it works + +`ShpfyPayments.Codeunit.al` orchestrates the sync in a deliberate sequence: first backfill Payout Id on orphaned transactions, then update statuses of non-terminal payouts, then pull new transactions, and finally pull new payouts. This ordering matters because transactions arrive before the payout that groups them, so the backfill step patches up the relationship retroactively. Transactions and payouts are both fetched via cursor-based GraphQL pagination through `ShpfyPaymentsAPI.Codeunit.al`, with batched ID lookups capped at 200 records per call. + +Disputes follow a similar pattern -- `SyncDisputes` first refreshes all non-terminal disputes (neither Won nor Lost), then imports new ones since the last known Id. Payment terms are a separate concern handled by `ShpfyPaymentTermsAPI.Codeunit.al`, which pulls payment terms templates from Shopify and maps them to BC Payment Terms codes via `ShpfyPaymentTerms.Table.al`. + +## Things to know + +- The Payout table (`ShpfyPayout.Table.al`) carries a detailed fee/gross breakdown across six categories: adjustments, charges, refunds, reserved funds, retried payouts, plus External Trace Id for bank reconciliation. +- Payment transactions link to payouts via `Payout Id` and to orders via `Source Order Id`. The `Invoice No.` FlowField resolves through Sales Invoice Header using the Shopify Order Id. +- Dispute records (`ShpfyDispute.Table.al`) are `Access = Internal` and reference the originating order via `Source Order Id` with a direct TableRelation to `Shpfy Order Header`. +- Payment terms enforce a single-primary constraint -- the `Is Primary` validation on `ShpfyPaymentTerms.Table.al` errors if another primary already exists. +- All enum conversions in the API codeunit fall back to `Unknown` when Shopify sends an unrecognized value, avoiding import failures from new upstream statuses. diff --git a/src/Apps/W1/Shopify/App/src/Products/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Products/docs/CLAUDE.md new file mode 100644 index 0000000000..0ec8bb77dc --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Products/docs/CLAUDE.md @@ -0,0 +1,19 @@ +# Products + +This module handles bidirectional product synchronization between BC Items and Shopify Products/Variants. Export pushes item data, prices, images, metafields, and translations to Shopify. Import retrieves Shopify products and either maps them to existing BC items or auto-creates new ones. + +## How it works + +Export runs from `ShpfyProductExport.Codeunit.al` (triggered by `ShpfySyncProducts.Report.al`). It iterates all `Shpfy Product` records that have a linked `Item SystemId`, calls `FillInProductFields` to populate title, vendor, product type, body HTML, tags hash, and status, then does a field-by-field `RecordRef` comparison (`HasChange`) against the previously synced state. Only changed products are sent to Shopify via `ProductApi.UpdateProduct`. The variant loop follows: existing variants are updated, missing ones created, and all are batched into a temp table before a single `UpdateProductVariants` call. When `OnlyUpdatePrice` is true, the export skips all non-price fields and uses GraphQL bulk operations for speed, falling back to per-variant calls if the bulk mutation fails. + +Import runs from `ShpfyProductImport.Codeunit.al`. It retrieves a product from Shopify, then iterates its variants. For each variant, `ShpfyProductMapping` attempts to find a matching BC item using the shop's SKU Mapping strategy (Item No., Barcode, Vendor Item No., Variant Code, or Item No. + Variant Code). If a mapping is found and `Shopify Can Update Items` is enabled, `ShpfyUpdateItem` runs. If no mapping is found and `Auto Create Unknown Items` is enabled, `ShpfyCreateItem` runs. + +## Things to know + +- Products link to BC items via `Item SystemId` (Guid), not `Item No.`. The `Item No.` field on `Shpfy Product` is a FlowField that looks up the item by SystemId. This means renumbering an item does not break the link. +- The `Last Updated by BC` timestamp on product and variant records prevents sync loops -- when the export updates a product, this timestamp is set so the next import cycle can skip recently-pushed changes. +- Change detection for the product body and tags uses integer hash fields (`Description Html Hash`, `Tags Hash`, `Image Hash`) stored on the product record. The `ShpfyHash` codeunit computes these. +- Shopify limits products to 3 option slots. Item attributes marked with `Shpfy Incl. in Product Sync = "As Option"` fill these slots. The export validates that all item variants have unique attribute combinations and that no variant is missing a value. See `CheckItemAttributesCompatibleForProductOptions` in `ShpfyProductExport`. +- UoM-as-variant mode (`Shop."UoM as Variant"`) consumes one option slot for the unit of measure. The `UoM Option Id` field on the variant (1, 2, or 3) tracks which option slot holds the UoM value, and this value determines UoM resolution during order mapping in the Order handling module. +- When a BC item is blocked, the `Action for Removed Products` shop setting controls behavior: `StatusToArchived` archives the Shopify product, `StatusToDraft` sets it to draft, and `DoNothing` skips the item entirely. The `IRemoveProductAction` interface dispatches this. +- The `Shpfy Shop Collection Map` table (30128) is obsolete as of version 28. Collections are now managed through `Shpfy Product Collection` (30163) with an item filter blob. diff --git a/src/Apps/W1/Shopify/App/src/Products/docs/data-model.md b/src/Apps/W1/Shopify/App/src/Products/docs/data-model.md new file mode 100644 index 0000000000..6852ae7a25 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Products/docs/data-model.md @@ -0,0 +1,54 @@ +# Data model + +## Overview + +The product data model bridges two identity systems: BC uses `Item SystemId` (Guid) as the stable link, while Shopify uses BigInteger IDs for products, variants, and inventory items. Every Shopify record carries both identifiers, and a set of hash fields enable cheap change detection without re-comparing blob contents. + +## Product-variant-inventory chain + +```mermaid +erDiagram + PRODUCT ||--o{ VARIANT : "has" + VARIANT ||--o| INVENTORY_ITEM : "tracked by" + PRODUCT ||--o{ PRODUCT_COLLECTION : "belongs to" + + PRODUCT { + } + VARIANT { + } + INVENTORY_ITEM { + } + PRODUCT_COLLECTION { + } +``` + +`Shpfy Product` (30127) is the central record. It holds the Shopify product ID, shop code, and a `Item SystemId` Guid that points to the BC Item. The `Item No.` field is a FlowField with a CalcFormula lookup via SystemId -- it is not stored. Hash fields (`Image Hash`, `Tags Hash`, `Description Html Hash`) are integer hashes computed by `ShpfyHash` and stored on the record so the export can skip unchanged products without comparing blob content. + +`Shpfy Variant` (30129) carries dual identity: `Item SystemId` for the BC item and `Item Variant SystemId` for the BC item variant. When a variant maps to an item without a BC variant (e.g., a single-variant product), `Mapped By Item` is set to true and `Item Variant SystemId` is cleared. Up to three option name/value pairs (`Option 1..3 Name`, `Option 1..3 Value`) represent Shopify product options. The `UoM Option Id` field (1, 2, or 3) indicates which option slot holds the unit of measure when UoM-as-variant mode is active -- this is critical for the order mapping module to resolve the correct UoM on imported order lines. + +`Shpfy Inventory Item` (30126) exists for every variant regardless of whether inventory tracking is enabled. It captures Shopify's internal inventory concept: country/region of origin, tracking state, and unit cost. It is keyed by its own Shopify ID and links back to the variant via `Variant Id`. + +## Collections + +```mermaid +erDiagram + PRODUCT_COLLECTION ||--o{ PRODUCT : "filters" + + PRODUCT_COLLECTION { + } + PRODUCT { + } +``` + +`Shpfy Product Collection` (30163) represents Shopify collections. The `Default` flag controls whether newly exported products are automatically assigned to the collection. The `Item Filter` blob stores a BC filter expression that determines which items belong to the collection during export. This replaced the older `Shpfy Shop Collection Map` (30128), which is obsolete since version 28 and removed in 31. + +## SKU mapping strategies + +The `Shpfy SKU Mapping` enum (30132) defines how the import side resolves a Shopify variant's SKU to a BC item. The mapping strategies are: `Item No.`, `Variant Code`, `Item No. + Variant Code` (split by the shop's `SKU Field Separator`), `Vendor Item No.` (checked against the Item Vendor table and item references), and `Bar Code` (checked against item references of barcode type). The `DoFindMapping` procedure in `ShpfyProductMapping.Codeunit.al` implements a fallback chain: first it tries the configured SKU strategy, then falls back to barcode lookup regardless of strategy. This means a barcode match can succeed even when SKU mapping is set to a different mode. + +## Interface-driven extensibility + +Two interfaces govern product lifecycle behavior during export: + +- `ICreateProductStatusValue` determines whether a newly created product starts as Active or Draft. The implementations `ShpfyCreateProdStatusActive` and `ShpfyCreateProdStatusDraft` simply return the corresponding enum value based on the item record. +- `IRemoveProductAction` determines what happens to the Shopify product when a BC item is deleted or the product record is removed. Implementations are `ShpfyToArchivedProduct`, `ShpfyToDraftProduct`, and `ShpfyRemoveProductDoNothing`. The action fires from the product table's `OnDelete` trigger, not from the export codeunit. diff --git a/src/Apps/W1/Shopify/App/src/Shipping/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Shipping/docs/CLAUDE.md new file mode 100644 index 0000000000..df8d575560 --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Shipping/docs/CLAUDE.md @@ -0,0 +1,17 @@ +# Shipping + +Shipment method mapping, shipping charges import, and fulfillment export to Shopify. This area bridges BC's shipping agents and shipment methods with Shopify's shipping lines, and pushes tracking information back to Shopify when orders are shipped. + +## How it works + +`ShpfyShippingCharges.Codeunit.al` pulls shipping lines from Shopify orders via GraphQL and populates `Shpfy Order Shipping Charges` records. For each shipping line, it auto-creates a `Shpfy Shipment Method Mapping` row keyed by (Shop Code, Title) if one doesn't exist, mirroring how the Transactions area auto-creates gateway mappings. The mapping table (`ShpfyShipmentMethodMapping.Table.al`) lets the user configure a BC Shipment Method Code, a Shipping Agent + Service Code, and a Shipping Charges account (G/L Account, Item, or Item Charge) per Shopify shipping method. + +The export side is handled by `ShpfyExportShipments.Codeunit.al` (covered in Order Fulfillments), which reads the Shipping Agent from the Sales Shipment Header to determine the tracking company and URL. `ShpfyShippingEvents.Codeunit.al` provides the `OnBeforeRetrieveTrackingUrl` and `OnGetNotifyCustomer` integration events for partners to override tracking URL generation and customer notification behavior. + +## Things to know + +- Shipping charges carry dual-currency amounts (shop money and presentment money) with discount amounts tracked separately, resolved through the order header's currency codes. +- The `Shpfy Shipping Agent` table extension adds a `Shpfy Tracking Company` enum field to BC's Shipping Agent, mapping to Shopify's known carrier names. When the enum is blank, the agent's Name (or Code) is sent as a custom tracking company. +- `ShpfySyncShipmToShopify.Report.al` is the user-facing entry point for pushing shipments to Shopify -- it is a report object (not a codeunit), following the connector's pattern of using reports as batch job wrappers. +- When shipping lines are removed from a Shopify order, `DeleteRemovedShippingLines` adjusts the order header's `Shipping Charges Amount` and `Total Amount` to keep them consistent. +- The `Sales Shipment Header` and `Sales Shipment Line` table extensions add Shopify Order Id and Fulfillment Id fields that link BC shipments to Shopify fulfillments. diff --git a/src/Apps/W1/Shopify/App/src/Transactions/docs/CLAUDE.md b/src/Apps/W1/Shopify/App/src/Transactions/docs/CLAUDE.md new file mode 100644 index 0000000000..75e37d539e --- /dev/null +++ b/src/Apps/W1/Shopify/App/src/Transactions/docs/CLAUDE.md @@ -0,0 +1,17 @@ +# Transactions + +Order-level payment transactions -- the individual authorization, capture, sale, void, and refund events on a Shopify order. This is where the gateway + credit card company composite key determines the BC payment method, and where the connector decides whether a Cust. Ledger Entry has consumed a transaction. + +## How it works + +`ShpfyTransactions.Codeunit.al` fetches all transactions for an order via `GetOrderTransactions` GraphQL and processes each one through `ExtractShopifyOrderTransaction`. For every transaction, it auto-creates lookup records in `Shpfy Transaction Gateway` and `Shpfy Credit Card Company` if they don't exist, and ensures a `Shpfy Payment Method Mapping` row is present for the (Shop Code, Gateway, Credit Card Company) triple. This means the mapping table grows organically as new gateways and card types appear in orders. + +The `ShpfyOrderTransaction.Table.al` carries dual-currency amounts (`Amount` / `Currency` in shop money, `Presentment Amount` / `Presentment Currency` in buyer money) plus rounding amounts in both currencies. Parent-child relationships between transactions are tracked via `Parent Id` (a capture links to its auth, a refund links to its capture). The `Used` FlowField checks `Cust. Ledger Entry` for a matching `Shpfy Transaction Id`, and `Manual Payment Gateway` flags cash/bank transfer transactions. + +## Things to know + +- The payment method mapping key is the triple (Shop Code, Gateway, Credit Card Company) in `ShpfyPaymentMethodMapping.Table.al`. The `Payment Method` FlowField on the transaction resolves through this mapping. +- `ShpfySuggestPayments.Codeunit.al` transfers the `Shpfy Transaction Id` to Cust. Ledger Entry during journal posting, and clears it on reversal. This is how the `Used` FlowField works. +- The `Gift Card Id` field is extracted from `receiptJson` (not from the main GraphQL fields) in `ExtractShopifyOrderTransaction`, which is easy to miss if reading the code quickly. +- Credit card detail fields (`Credit Card Bin`, `AVS Result Code`, `CVV Result Code`) are `Access = Internal`, so they exist for data capture but are not exposed to extensions. +- The `Priority` field on the mapping table and the `Source Name` field on the transaction table have been removed (ObsoleteState = Removed, tag 28.0).