OUT-3816: collapse product mapping table to one row per product - #258
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR collapses the QB product mapping table from one row per price to one row per Assembly product. Price fields are removed from the mapping table, the API, and the save flow — invoice lines carry their own
Confidence Score: 5/5Safe to merge once the pending smoke test with a multi-price product is complete; the regression fixes are correct and automated tests are clean. The three regression fixes added in the last commit (and() for mappedConditions, unsetTransaction() before early return, sparse:true on the webhook update) are all correct and well-targeted. The core simplification — one row per product, no price in the mapping path — is consistent across the service, hooks, UI, DTO, and schema validation. No new defects are introduced; the orphaned DB columns (priceId, copilotUnitPrice) remain a cleanup item but don't cause runtime errors. src/db/schema/qbProductSync.ts still declares priceId and copilotUnitPrice columns without a migration to drop them; a follow-up is warranted once the stacked PRs merge. Important Files Changed
Sequence DiagramsequenceDiagram
participant Browser
participant FlattenRoute as /product/flatten
participant MapRoute as /product/map
participant ProductService
participant CopilotAPI
participant DB as DB (QBProductSync)
participant QB as QuickBooks API
Note over Browser,QB: Load mapping table (simplified flow)
Browser->>FlattenRoute: GET /product/flatten
FlattenRoute->>ProductService: getProductsForMapping()
ProductService->>CopilotAPI: "getProducts(limit=1000)"
CopilotAPI-->>ProductService: products[]
ProductService-->>FlattenRoute: "{ id, name, description }[]"
FlattenRoute-->>Browser: one row per product (no prices)
Note over Browser,QB: Save mapping
Browser->>MapRoute: POST /product/map
MapRoute->>ProductService: handleProductMap(body)
ProductService->>DB: "upsert WHERE productId (unitPrice=null always)"
DB-->>ProductService: saved rows
ProductService-->>MapRoute: updated mappings
MapRoute-->>Browser: 200 OK
Note over Browser,QB: Webhook: product name/description update
QB->>ProductService: webhookProductUpdated(resource)
ProductService->>DB: query mapped products (AND qbItemId NOT NULL, qbSyncToken NOT NULL)
DB-->>ProductService: mapped rows
ProductService->>QB: "itemFullUpdate({ sparse: true, Name, Description })"
Note right of QB: sparse=true prevents UnitPrice reset to 0
QB-->>ProductService: updated item
ProductService->>DB: update qbSyncToken
Reviews (2): Last reviewed commit: "fix(OUT-3816): combine webhookProductUpd..." | Re-trigger Greptile |
- Show one row per Assembly product; match mappings by productId (drop priceId predicates in useMapItem / useProductTableSetting / selectItem). - Remove all prices from the table (Assembly product price and QB item price subtitle/dropdown). - getProductsForMapping (was getProductsWithPrices) returns one row per product and no longer fetches Copilot prices; flatten DTO is name-only. - Drop priceId plumbing from MapItemComponent; controller + flatten route renamed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove the "unitPrice is required when isExcluded is false" superRefine from QBProductCreateArraySchema — items are created at price 0 now, so a mapped row legitimately has no unitPrice. - Remove numericPrice from QBItemSchema (changedItemReference.qbItem); its only consumer was handleProductMap, which now stores unitPrice: null. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- selectItem and useProductTableSetting always set unitPrice: null — items are price-0 and price isn't tracked in the UI. - Drop the now-dead price/numericPrice fields from QBItemDataType and formatQBItemForListing, and the numericPrice passed into selectItem. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- webhookProductUpdated now sends sparse:true so a name/description update no longer resets the QB item's UnitPrice to 0 (mapped items store null unitPrice now). - handleProductMap calls unsetTransaction() before the initial-save early return, so the DB singleton isn't left on a closed tx. - Drop the now-unused createdAt from the flatten DTO + getProductsForMapping. - Remove stale numericPrice from the productKeyed test fixture; fix stale handleProductMap comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mapped-products filter used the comma operator, so only the qbSyncToken guard survived and the qbItemId guard was discarded — truly unmapped rows (null qbItemId) could slip into the update loop. Use and() to apply both guards. Also drop the stale "updates redis" comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2325b7d to
876f27a
Compare
| const formatted = (products?.data ?? []).map((product) => ({ | ||
| id: product.id, | ||
| name: product.name, | ||
| description: convert(product.description), | ||
| })) | ||
|
|
||
| return { products: formatted } |
There was a problem hiding this comment.
| const formatted = (products?.data ?? []).map((product) => ({ | |
| id: product.id, | |
| name: product.name, | |
| description: convert(product.description), | |
| })) | |
| return { products: formatted } | |
| reeturn (products?.data ?? []).map((product) => ({ | |
| id: product.id, | |
| name: product.name, | |
| description: convert(product.description), | |
| })) |
We are returning products here. No need to make it an object imo.
There was a problem hiding this comment.
Letting this be for now.
Convert CopilotAPI._getProducts from positional (name, nextToken, limit) optionals to a single options object and update all callers (product service + backfill/sync cmd scripts). Add defensive optional chaining on products[index] in the product mapping hook. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… price columns OUT-3815: add partial unique index uq_qb_product_sync_product_active on (portal_id, product_id) WHERE deleted_at IS NULL. OUT-3819: drop the vestigial price_id, unit_price, copilot_unit_price columns and remove the legacy unitPrice read in webhookProductUpdated. Both schema changes are folded into one migration (20260603102427). The one-time dedup SQL is hand-run against prod before deploy (gitignored snippet). Hardening in product.service.ts: - guard the initial-save batch insert in handleProductMap with onConflictDoNothing scoped to the partial index, so a re-fired/concurrent save no-ops instead of 500-ing. - wrap both db.transaction callbacks in try/finally so unsetTransaction() always restores the db singleton, even on a throw or early return. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The initial-save insert uses onConflictDoNothing, so RETURNING omitted rows skipped on a re-fired/concurrent save and the client could receive []. Always return getAll() instead; drop the now-unused returningFields param. Add a regression test covering a repeated initial save. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What & why
Part of OUT-3787 (simplified product mapping). The mapping settings table previously rendered one row per price; this collapses it to one row per Assembly product and removes price from the table entirely (per product → one QB item; invoice lines carry their own price).
Changes
productId(droppedpriceIdpredicates inuseMapItem/useProductTableSetting/selectItem).getProductsForMapping(wasgetProductsWithPrices) returns one row per product and no longer fetches Copilot prices; flatten DTO is name-only.unitPrice is required when isExcluded is falsesuperRefine andnumericPricefromQBItemSchema.unitPriceis now alwaysnullthrough the UI mapping path andhandleProductMap; removed deadprice/numericPricefromQBItemDataType/formatQBItemForListing.Regression review fixes (last commit)
webhookProductUpdatednow sendssparse: trueso a name/description update doesn't reset a mapped QB item'sUnitPriceto0(mapped items storenullunitPrice now).handleProductMapcallsunsetTransaction()before the initial-save early return (no closed-tx singleton leak).numericPricefrom a test fixture; dropped the unusedcreatedAtfrom the flatten DTO.Verification
tscclean · full suite 286 passing · lint/prettier clean.🤖 Generated with Claude Code