Skip to content

feat(business): add catalog, product and business profile operations - #1252

Merged
jlucaso1 merged 15 commits into
mainfrom
claude/business-catalog-products-orders-0mb890
Aug 8, 2026
Merged

feat(business): add catalog, product and business profile operations#1252
jlucaso1 merged 15 commits into
mainfrom
claude/business-catalog-products-orders-0mb890

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Business existed here only for reading a profile and parsing notifications. This adds:

  • get_catalog — one page of a business's products, with cursor paging.
  • get_collections — one page of product collections, each with its items inline.
  • get_order — an order's line items and totals, from the id + token an order message carries.
  • update_profile — delta mutation of address, latitude/longitude, description, email, websites, categories and opening hours.
  • set_cover_photo / remove_cover_photo.

Left out of this slice: product create/edit/delete, and the image-upload leg of the cover photo. Both are explained under Checked and not changed and Media — neither is a shortcut, both are blocked on ground truth rather than on effort.

Ground truth

The MEX-vs-IQ question resolves per operation, not globally. Evidence is WA Web 2.3000.1044659339 — whatspec IR from a fresh clone, plus the full 497-file pinned bundle set fetched from bundles.lock.json and grepped directly.

Operation Route Identifier
Catalog MEX WAWebQueryCatalogQuery
Collections MEX WAWebQueryProductCollectionsQuery
Order MEX WAWebBizQueryOrderJobQuery
Profile edit, cover photo IQ w:bizbusiness_profile v="3" mutation_type="delta"
Product create/edit/delete neither absent from the bundle

Why MEX for catalog/collections/order. w:biz:catalog still exists in the bundle, but the IR extracts only two stanzas under it — QueryGetSignedUserInfo and QueryProductListCatalog (a by-id status query). Baileys' product_catalog, collections, product_catalog_add/edit/delete are not there. Decisively, the catalog query has no IQ fallback: xwa_product_catalog_get_product_catalog failing raises CatalogUnknownError. The one operation that does fall back is get_product_list ("xwa_product_catalog_get_product_list failed, IQ fallback"), and its A/B prop graphql_get_product_list = false — so the surviving w:biz:catalog IQ is exactly the one MEX op still gated off. The two transports are the same server schema: Baileys' IQ bodies match the MEX variablesShape field-for-field (jid/limit/width/height/after; biz_jid/collection_limit/item_limit). Baileys implements the legacy smax-IQ transport of a schema WA Web has since moved to MEX.

Why IQ for the profile. Here Baileys is right and the whatspec IQ extractor is wrongw:biz/business_profile is built through WAWap.wap(...) rather than the standard builder, so it lands in the IR's "iq builder substring present but no AST iq call": 5 drop bucket. The bundle has it plainly, including fields Baileys lacks (latitude, longitude, categories, price_tier, service_areas) and a detail it gets wrong: WA Web emits at most two <website/> nodes, and a lone empty <website/> to clear the list. BUSINESS_PROFILE_MAX_WEBSITES and a typed error encode that.

Correction to the pre-research in the task. graph_ql_product_catalog_get_public_key, report_product and biz_create_order are real, and out of scope as stated.

Vendored MEX operations refreshed

The task's pre-research noted mex_operations.rs was stale (WA 2.3000.1042742319). I originally filed that as a separate chore; that was wrong, and the check that corrected it is worth stating: of the 11 MEX operations this repo actually calls, query_catalog is the only one whose persisted-document id had rotated (991655328839478230445081048424116). The other ten are current. So the one stale id sat on precisely the query this PR introduces — a defect in this change, not background drift.

mex_operations.rs is vendored verbatim from whatspec's generated operations.rs, so this refreshes the whole file rather than editing the constant: the header forbids hand-edits, and a mixed-vintage file would misreport which WhatsApp build it describes.

The regeneration is verified, not assumed. A first run against 494 bundles was refused by whatspec's own integrity gate — appstate extracted 0 actions, because three bundles had silently failed to download (their filenames exceed the 255-byte limit). With the complete 497, the run succeeds and the regenerated mex IR matches the committed whatspec snapshot byte for byte, so the output reflects that snapshot rather than an incidental fetch. Blast radius: one in-use id changes, ten unchanged, fetch_native_ads_mvp_eligibility drops with zero references in the tree, nine new ops arrive with no callers.

Design

Money is the part worth reviewing. WhatsApp scales money by 1000, not 100priceAmount1000 is an int64 in the protobuf, and the client formats through formatAmount1000 / u/1e3. The GraphQL reader is h(e) = typeof e === "string" ? (e.length > 0 ? parseInt(e,10) : null) : e, which pins three things: the scalar is a decimal string, a bare number is tolerated in the same position, and empty string means absent, not zero. Price is therefore { amount_1000: i64, currency: Option<String> }, never a float, and dividing for display is the caller's decision.

Optional means Option, including booleans. Only Product::id is guaranteed — WA Web asserts on it, so a product without one is a typed error rather than a skipped row. Everything else is Option, because a false for an absent flag is as much a sentinel as an empty string. This matters concretely: the IR mirrors is_hidden, is_sanctioned, belongs_to and can_appeal as booleans, and all four are strings on the wireis_hidden uses its own spelling ISHIDDEN_TRUE, the rest are "true"/"false". The parser accepts both spellings and leaves anything unrecognised None.

ProductAvailability is a WireEnum over the GraphQL names (IN_STOCK, OUT_OF_STOCK, AVAILABLE_FOR_ANOTHER_POSTCODE) with a fallback variant, so a new value degrades one field instead of failing the page.

Paging is asymmetric between the two queries, and the types say so rather than smoothing it over: the catalog returns after and before, the collections response carries after only.

A second IR mirror correction, this one affecting the request. WA Web stringifies limit, width, height, collection_limit and item_limit, and sends allow_shop_source as ALLOWSHOPSOURCE_TRUE/_FALSE. The generated Variables type them as i64/bool. The catalog and collection queries build their variables by hand for that reason; the order query, whose image_dimensions genuinely are numbers, uses the generated type. Tests assert is_string() / is_number() so this cannot silently regress.

Malformed responses fail rather than degrade. A non-object collection entry, or a products field present but not an array, are typed errors — either would otherwise read back as a plausible empty collection. An absent products key is still a legitimately empty collection.

Values that would poison a delta are rejected before any node is built. Two cases, one failure mode: the server rejects the whole business_profile delta on a bad field, silently losing the unrelated fields in the same update. Coordinates are range-checked because f64::to_string would otherwise render NaN/inf verbatim. Opening times are bounded to the day: WAWebSmbUtilsTimeUtils defines minutesToTime as startOf("day").add(n, "minutes") and its inverse parses a within-day format, so the domain is minutes past local midnight and 1439 is the largest value WA Web emits. Ranges crossing midnight stay legal — WA Web's editor coerces open > close in its picker, but that is a UI affordance, not a wire constraint other clients share.

Page-size and thumbnail defaults are this client's choice, called out as such in the code: WA Web derives them at each call site from the surface it is rendering, and no A/B prop or bundle constant backs them.

Media

Not covered — reported rather than improvised. biz-cover-photo is a real media type in WA Web's list, and the upload returns { fbid, meta_hmac, ts }, which the IQ quotes back as cover_photo@id/token/ts. But RawUploadResponse in src/upload.rs requires url + direct_path, which that endpoint does not return, so the existing pipeline cannot perform this upload — a genuine sub-gap, not something to fake with a new media type.

So set_cover_photo takes a CoverPhotoUpload receipt instead of image bytes: the IQ is a complete, tested protocol contract, and closing the upload gap later makes it callable end-to-end without redesign. remove_cover_photo needs no upload and works today.

Checked and not changed

  • Product create/edit/deleteproduct_catalog_add/_edit/_delete return zero hits across all 497 bundles, and there is no MEX mutation for them either. Only Baileys has these; they are not a WA Web operation. Implementing them would mean shipping a wire format with no ground truth behind it, and their image path depends on the upload gap above. Deferred deliberately, per the task's "half-delivery" guidance.
  • report_product, graph_ql_product_catalog_get_public_key, biz_create_order — confirmed real, out of scope, no consumer.
  • QueryProductListCatalog — real, and interesting (the last live catalog IQ, with a gated MEX twin), but it is a by-id status query, not catalog listing.

Validation

cargo fmt --all --check
cargo test -p wacore --lib            # 1392 passed
cargo test -p whatsapp-rust --lib     # 1489 passed
cargo test --doc -p wacore -p whatsapp-rust
cargo clippy -p wacore -p whatsapp-rust --all-features --all-targets -- -D warnings   # clean

36 new tests: request construction and response parsing per operation, malformed/missing/unknown-field failures as typed errors, delta values rejected before serialization (non-finite and out-of-range coordinates, opening times outside the day, with midnight-crossing ranges asserted still accepted), and a precision test using 2^53+1 thousandths that fails if the amount ever passes through an f64. Fixtures are hand-written from the response shapes, with fictitious ids, names and JIDs.

Full CI was green on 563856c (26/26). Since then this branch merged main (8e59de5) — both sides had appended to the same sorted re-export list in src/lib.rs, resolved as the union so neither the business types nor upstream's NewsletterAdminInfo/NewsletterAdminProfile/NewsletterFollower are lost. The counts above are post-merge.

cargo clippy --workspace cannot run in this container — alsa-sys fails to build for want of a system ALSA dev package (CI installs it explicitly; the dependency comes from the voip-cli example, unrelated to this change). Scoped to the two crates touched instead, with --all-features to match what CI enforces. cargo-nextest was likewise unavailable, so cargo test was used.

Adds the read side of the business catalog over MEX and the write side of
the business profile over IQ, matching how WhatsApp Web splits the two.

Catalog, collections and order lookup are MEX persisted queries
(QueryCatalog, QueryProductCollections, BizQueryOrder). WhatsApp Web has
no IQ fallback for them: a failed xwa_product_catalog_get_product_catalog
raises CatalogUnknownError rather than retrying over w:biz:catalog, and
the only catalog IQ left in the bundle is product_list plus
signed_user_info.

Business profile writes stay on IQ w:biz as a business_profile node with
v="3" and mutation_type="delta", covering address, coordinates,
description, email, websites, categories and opening hours, plus setting
and removing the cover photo.

Two shapes the generated mex Variables mirrors get wrong, so the catalog
and collection queries build their variables by hand: WhatsApp Web sends
limit, width, height, collection_limit and item_limit as strings, and
allow_shop_source as ALLOWSHOPSOURCE_TRUE/_FALSE rather than a boolean.
The order query does send numeric dimensions and uses the generated type.

Money is kept as an i64 in thousandths of the currency unit, matching
priceAmount1000 in the protobuf and formatAmount1000 in the client, so a
price is never routed through a float. Absent fields are Option rather
than a blank or zero, including the booleans, which arrive as strings
(is_hidden is the enum spelling ISHIDDEN_TRUE, not a JSON boolean).

Product create/edit/delete is deliberately absent: those IQs are not in
the WhatsApp Web bundle at all, and the cover-photo upload leg is not
covered either, since biz-cover-photo returns fbid/meta_hmac/ts instead
of the url/direct_path pair the upload pipeline parses. set_cover_photo
therefore takes an upload receipt rather than image bytes.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a0f66363-e494-4505-9b56-ed7ba3e186c2

📥 Commits

Reviewing files that changed from the base of the PR and between 9ff9126 and 40af422.

📒 Files selected for processing (1)
  • src/features/business.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added WhatsApp Business tools for browsing catalogs, collections, products, and orders with pagination.
    • Added business profile management, including hours, categories, websites, coordinates, and cover photos.
    • Added typed pricing, availability, media, order, collection, and product information.
    • Added support for contact profiles, newsletter labels, passkey checks, and team-link invitations.
  • Improvements

    • Updated supported operations and strengthened validation for profile updates and malformed responses.
    • Expanded public access to business, catalog, product, pricing, and profile capabilities.
    • Improved support for batch inputs and refreshed related operation responses.

Walkthrough

The PR adds typed business catalog, collection, order, profile, and cover-photo APIs. It adds parsing, validation, paging, pricing, tests, and public exports. It also refreshes generated MEX operations and input schemas.

Changes

Business API

Layer / File(s) Summary
Business profile mutations
wacore/src/iq/business.rs
Adds validated profile delta updates and cover-photo mutations with IQ serialization and tests.
Catalog, collection, and order API
src/features/business.rs
Adds typed models, MEX-backed reads, request builders, tolerant parsers, malformed-response checks, paging, pricing, and tests.
Public business feature wiring
src/features/mod.rs, src/lib.rs
Registers the business module and re-exports its public APIs and related types.

MEX operation schema refresh

Layer / File(s) Summary
New and updated MEX operations
wacore/src/iq/mex_operations.rs
Adds customer-profile, Labyrinth, newsletter, passkey, and team-link operations. Updates operation names, document IDs, and response models.
MEX input and document updates
wacore/src/iq/mex_operations.rs
Changes selected inputs from strings to string vectors and updates related operation schemas.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Business
  participant MEXOperations
  participant IQTransport
  participant Parser
  Client->>Business: request catalog, collection, or order
  Business->>MEXOperations: send typed MEX query
  MEXOperations-->>Business: return response
  Business->>Parser: validate and parse response
  Parser-->>Client: return typed result
  Client->>Business: update profile or cover photo
  Business->>IQTransport: send business IQ mutation
  IQTransport-->>Client: return mutation result
Loading

Possibly related PRs

Suggested labels: api-design, breaking-change, size-increase-ok

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding business catalog, product, and profile operations.
Description check ✅ Passed The description directly explains the added business operations, design decisions, scope, validation, and test results.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/business-catalog-products-orders-0mb890

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds typed business APIs for catalog, collection, order, profile, and cover-photo operations. The previous collection-pagination issue is fixed by exposing the response cursor and accepting it on subsequent requests.

  • Adds MEX-backed catalog, collection, and order queries with typed response parsing.
  • Adds IQ-backed business-profile and cover-photo mutations.
  • Refreshes generated MEX operation metadata and public exports.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported collection-pagination issue is fixed by returning the forward cursor and accepting it in the next request.

Important Files Changed

Filename Overview
src/features/business.rs Adds the public business feature API and parsers; collection pagination now preserves paging.after and serializes it on the next request.
wacore/src/iq/business.rs Adds typed IQ specifications and validation for business-profile and cover-photo mutations.
wacore/src/iq/mex_operations.rs Refreshes generated persisted-query definitions used by the new MEX-backed operations.
src/features/mod.rs Registers the business feature and re-exports its public types.
src/lib.rs Exposes the new business API types from the crate root.

Sequence Diagram

sequenceDiagram
  participant App
  participant Business
  participant MEX
  participant WhatsApp
  App->>Business: get_collections(options)
  Business->>MEX: collections request with options.after
  MEX->>WhatsApp: persisted query
  WhatsApp-->>MEX: collections + paging.after
  MEX-->>Business: response data
  Business-->>App: "Collections { collections, after_cursor }"
  App->>Business: get_collections(after_cursor)
Loading

Reviews (15): Last reviewed commit: "fix(business): keep the shimmed product ..." | Re-trigger Greptile

Comment thread src/features/business.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread wacore/src/iq/business.rs
Comment thread src/features/business.rs Outdated
Comment thread src/features/business.rs Outdated
Comment thread src/features/mod.rs
Comment thread src/features/business.rs Outdated
…dation

Addresses review findings on the catalog slice.

get_collections sent CollectionOptions::after but returned a bare
Vec<Collection>, discarding the paging cursor the server replies with, so
a business with more collections than collection_limit looked complete
and could not be paged past the first response. WhatsApp Web reads
paging.after here, so the response now returns a Collections page
carrying the cursor. Unlike the catalog this paging object has no
"before", and the type says so.

parse_collections also accepted two malformed shapes: a non-object entry
indexed to Null on every field and became a plausible nameless empty
collection, and a "products" that was present but not an array fell
through to empty rather than erroring the way the catalog path does.
Both are now typed errors. An absent "products" key is still a
legitimately empty collection.

Profile coordinates were formatted straight to wire text, so NaN and the
infinities would have been sent as "NaN"/"inf" and an out-of-range
latitude as a point that does not exist. The server rejects the whole
delta in that case, silently losing the other fields in the same update,
so coordinates are now range-checked before any node is built.

Finally, the profile-write types are the arguments and error of this
feature's public methods but lived only in wacore, forcing callers to
reach into wacore::iq::business to construct an update. They are now
re-exported through features::business and the crate root, matching the
convention the other features follow.

jlucaso1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

All five review findings were valid — fixed in 563856c.

Collections cursor (Greptile + cubic). Real bug, and the worst of the five: CollectionOptions::after was sent but the cursor was never returned, so a business with more collections than collection_limit looked complete and could not be paged. Confirmed against the bundle — WA Web reads paging.after here — and get_collections now returns a Collections page. Worth noting the asymmetry: this paging object has after only, no before like the catalog, and the type reflects that rather than inventing a symmetric one.

Non-object collection entry / non-array products. Both silently produced valid-looking data. Now typed errors. One distinction kept deliberately: a collection with no products key is legitimately empty, so only a key that is present and not an array is an error — that matches the catalog path, which requires products because there it is always sent.

Coordinate validation. f64::to_string would have put NaN/inf on the wire, and the failure mode is worse than a rejected coordinate: the server rejects the entire delta, so an unrelated description or email in the same update is lost too. Range-checked before any node is built. The abs() <= limit form rejects NaN on its own, since every NaN comparison is false.

Re-exports. Correct per the convention in AGENTS.mdBusinessProfileUpdate and friends are the arguments and error of public methods here, so they now come through features::business and the crate root instead of making callers reach into wacore::iq::business.


Generated by Claude Code

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 4 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Adds new public business APIs with protocol, currency, and delta-write tradeoffs (MEX-vs-IQ choice, amount_1000 scaling, profile mutation semantics); diffs are truncated, so the claimed ground truth and behaviors are not fully verifiable without human review.

Re-trigger cubic

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.02 MiB 10.02 MiB 0
bin .text 8.03 MiB 8.03 MiB 0
bin allocated (text+data+bss) 10.02 MiB 10.02 MiB 0
llvm-lines wacore 529,253 532,607 +3,354 (+0.63%) 🔺
llvm-lines wacore copies 17,281 17,373 +92 (+0.53%) 🔺
llvm-lines whatsapp-rust lib 744,002 755,147 +11,145 (+1.50%) ⚠️
llvm-lines whatsapp-rust lib copies 23,326 23,625 +299 (+1.28%) ⚠️
deps crates (Cargo.lock) 462 462 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.82 MiB 1.82 MiB 0
.text wacore 691.75 KiB 692.06 KiB +312 B (+0.04%) 🔺
.text wacore_binary 88.22 KiB 88.22 KiB 0
.text wacore_libsignal 178.88 KiB 178.88 KiB 0
.text wacore_appstate 22.35 KiB 22.35 KiB 0
.text wacore_noise 20.94 KiB 20.94 KiB 0
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 540.30 KiB 540.30 KiB 0
.text whatsapp_rust_tokio_transport 40.49 KiB 40.49 KiB 0
.text whatsapp_rust_ureq_http_client 12.68 KiB 12.68 KiB 0
.text std 992.99 KiB 992.99 KiB 0
.text other deps 1.90 MiB 1.90 MiB -312 B (-0.02%) 🔽

Baseline: c1c784170 (latest main run) · Head: 3efc99683 · Graphs

@jlucaso1

jlucaso1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 563856c80e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/features/business.rs
let response = self
.client
.mex()
.query(mex_request!(query_catalog, catalog_variables(jid, options)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refresh the catalog persisted-query ID

This newly exposed path obtains its persisted-query ID from query_catalog::DOC_ID, which is still 9916553288394782, even though the WhatsApp Web ground-truth check documented for this change found the current ID to be 30445081048424116. When the server no longer accepts the prior persisted document, every get_catalog call fails before response parsing, so refresh the vendored MEX operations before wiring this query.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, and I had the severity wrong — fixed in cdac3ac.

I'd disclosed the drift in the PR body but filed it as a separate chore on the assumption that every MEX consumer shared the exposure. That assumption was wrong, and checking it is what changed my mind: of the 11 operations this repo actually calls, query_catalog is the only one whose id had rotated — the other ten are current (repo-wide, 4 of 126 resolvable ops drifted). So the stale id sits on exactly the query this PR introduces, and no existing caller was ever exercising it. That makes it this PR's defect, not a background chore.

Fixed by refreshing the whole vendored file rather than editing the one constant: the header forbids hand-edits, and a mixed-vintage file would misreport which WhatsApp build it describes. Regenerated with whatspec at WA 2.3000.1044659339 from the pinned 497-bundle set.

Worth recording how that was verified, since a regeneration is only as trustworthy as its inputs. My first attempt ran against 494 bundles and whatspec refused to write — its integrity gate caught that appstate had extracted 0 actions. Three bundles had failed to download because their filenames exceed 255 bytes. With the full 497, the run succeeds and the regenerated mex IR matches the committed whatspec snapshot byte for byte, so the output reflects that snapshot rather than an incidental fetch.

Blast radius, checked rather than assumed: one in-use id changes (query_catalog), ten unchanged, fetch_native_ads_mvp_eligibility drops with zero references anywhere in the tree, nine new ops arrive with no callers. Build, 1386 + 1465 lib tests, doctests and clippy --all-features all clean.


Generated by Claude Code

Comment thread wacore/src/iq/business.rs
Comment on lines +123 to +124
open_time: Some(open_time),
close_time: Some(close_time),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid business-hour ranges before serialization

When a caller supplies a value outside a local day, such as u32::MAX, with_hours accepts it and the mutation builder serializes it verbatim as open_time or close_time. The server can then reject the entire profile delta, including unrelated fields in the same update; validate the verified minute bounds in the constructor or update specification rather than allowing every u32.

AGENTS.md reference: AGENTS.md:L53-L56

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in cdac3ac. It's the same failure mode as the coordinate check, and leaving one validated while the other took any u32 was an inconsistency in my own design.

I went to the bundle for the bound rather than assuming, because the unit determines it. WAWebSmbUtilsTimeUtils settles it: minutesToTime is moment.utc().startOf("day").add(n, "minutes"), and its inverse timeStringToMinutes parses a within-day format and returns duration(diff).asMinutes(). So the domain is minutes past local midnight, and 1439 (23:59) is the largest value WhatsApp Web can produce — hence value < 1440.

Validation went in the spec constructor rather than with_hours, matching where check_coordinate runs — before any node is built, so a bad value can't take an unrelated field in the same delta down with it, and with_hours stays infallible.

One thing I deliberately did not add, despite finding a precedent for it. WA Web's hours editor has an i>l branch that coerces one endpoint when open > close, so its picker never emits an inverted range. That is a UI affordance, not a wire constraint — a range crossing midnight (20:00–02:00 is the obvious real case) is legitimate, and other clients may well send one. Enforcing ordering here would reject valid data, so the check bounds each endpoint independently and (1200, 120) is covered by a test asserting it stays accepted.


Generated by Claude Code

…r times

Addresses the Codex review on the catalog slice.

query_catalog's persisted-document id had rotated upstream
(9916553288394782 -> 30445081048424116), so get_catalog — added in this
branch — was being wired to an id WhatsApp Web no longer sends. Of the
eleven mex operations this repo actually calls, it is the only one that
had drifted, so the new code path was the single exposed caller rather
than one of many.

mex_operations.rs is vendored verbatim from whatspec's generated
operations.rs, so this refreshes the whole file rather than editing the
id in place: the header forbids hand-edits, and a mixed-vintage file
would misreport which WhatsApp build it describes. Regenerated with
whatspec at WA 2.3000.1044659339 from the pinned 497-bundle set, whose
mex IR reproduces the committed whatspec snapshot byte for byte — the
refresh is faithful to that snapshot, not to an incidental fetch.

The refresh moves one other in-use id not at all (the remaining ten are
unchanged), drops fetch_native_ads_mvp_eligibility, which nothing
references, and adds nine operations with no callers yet.

Separately, open_time and close_time accepted any u32 and were formatted
straight to wire text, so a value past the end of the day would be sent
verbatim and the server would reject the entire profile delta, silently
losing the unrelated fields in the same update — the failure mode the
coordinate check already guards against. They are minutes past local
midnight: WhatsApp Web renders them as startOf("day").add(n, "minutes")
and only ever derives them by parsing a within-day time string, so 1439
is the largest value it emits. Ranges that cross midnight stay legal;
WA Web's editor coerces open > close in its own picker, but that is a UI
affordance rather than a wire constraint that other clients share.
@greptile-apps
greptile-apps Bot dismissed their stale review August 8, 2026 05:36

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread wacore/src/iq/business.rs
The business-hour variant was inserted between InvalidCoordinate's doc
comment and InvalidCoordinate itself, so both blocks stacked on the new
variant and the coordinate error documented nothing. Each variant now
carries its own rationale.
@greptile-apps
greptile-apps Bot dismissed their stale review August 8, 2026 05:42

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

Both sides added re-exports to the same sorted list in src/lib.rs: the
business catalog types here, and NewsletterAdminInfo,
NewsletterAdminProfile and NewsletterFollower upstream. Resolved as the
union of the two, so neither side loses an export.
@greptile-apps
greptile-apps Bot dismissed their stale review August 8, 2026 05:49

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d062a7a2b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/features/business.rs Outdated
Comment on lines +691 to +695
OrderProduct {
id: opt_str(&product["id"]),
name: opt_str(&product["name"]),
price: parse_price(&product["price"], currency.as_deref()),
quantity: opt_i64(&product["quantity"]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve selected variants in order line items

For orders containing product variants such as size or color, the persisted response exposes the selection through biz_query_order::Products.variant_info.variant_properties, but this conversion drops it entirely. Callers fulfilling such orders receive the base product, quantity, and price without knowing which variant was ordered, so expose the variant properties on OrderProduct and parse them here.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

Comment thread src/features/business.rs
.as_array()
.ok_or_else(|| BusinessError::malformed(OP, "order.products is not an array"))?
.iter()
.map(|product| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-object order line items

When order.products contains a malformed non-object entry, indexing that value returns Null for every field and this map produces a plausible OrderProduct with every optional field absent. This silently corrupts the order result instead of returning MalformedResponse, unlike the catalog and collections parsers, so check product.is_object() before constructing the line item.

Useful? React with 👍 / 👎.

Comment thread wacore/src/iq/business.rs
Comment on lines +114 to +118
pub fn with_hours(
day_of_week: DayOfWeek,
mode: BusinessHourMode,
open_time: u32,
close_time: u32,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Constrain range constructors to compatible hour modes

Because both constructors accept any BusinessHourMode, callers can create new(day, SpecificHours), which emits no required range, or with_hours(day, Open24H, ...), which emits times for a mode documented as carrying none. These are stanza shapes WhatsApp Web does not produce and can cause the server to reject the entire profile delta; specialize the constructors or validate the mode/time combination in BusinessProfileUpdateSpec.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

Comment thread src/features/business.rs Outdated
Comment on lines +35 to +38
pub use wacore::iq::business::{
BUSINESS_PROFILE_MAX_WEBSITES, BusinessHourMode, BusinessHoursConfig, BusinessHoursUpdate,
BusinessProfile, BusinessProfileUpdate, BusinessProfileUpdateError, CoverPhotoUpload,
DayOfWeek,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-export the nested business-profile result types

This re-export includes BusinessProfile but omits BusinessHours and BusinessCategory, even though they are the public types of its business_hours and categories fields. A whatsapp-rust caller that needs to name those result types in a signature must therefore add and import wacore, defeating the feature-level public surface this block is intended to provide; re-export both here and from the crate root.

AGENTS.md reference: AGENTS.md:L43-L43

Useful? React with 👍 / 👎.

Comment thread src/features/business.rs Outdated
Comment on lines +659 to +663
Ok(Collection {
id: opt_str(&collection["id"]),
name: opt_str(&collection["name"]),
products,
review_status: opt_str(&collection["status_info"]["status"]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve collection moderation details

For rejected or appealable collections, the persisted response's StatusInfo2 contains can_appeal, reject_reason, and commerce_url, but this conversion retains only status. A merchant therefore cannot learn why a collection was rejected or where and whether to appeal from get_collections; add these fields to Collection and parse the complete moderation status.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

…d mode pairing

Addresses the second Codex review. All five findings check out against the
whatspec IR at WA 2.3000.1044659339.

Order line items dropped variant_info.variant_properties, which the
persisted response does carry. Every variant of one listing shares an id,
name and price, so without the chosen size or colour an order for a
variant product cannot be fulfilled at all. OrderProduct now exposes them.

Collections kept only status_info.status and discarded can_appeal,
reject_reason and commerce_url. A rejection is only actionable with the
reason and the appeal route, and unlike the product status_info — which
really does carry just status and can_appeal — the collection object has
all four.

parse_order also indexed line items without checking their shape, so a
non-object entry read back as a plausible product with every optional
field absent, rather than erroring the way the catalog and collection
parsers do.

Business hours accepted any mode/time combination. WhatsApp Web emits
open_time and close_time exactly when a day has a range and reads them
back only for specific_hours, so a ranged open_24h — or a specific_hours
day with no range — is a stanza it never produces and the server can
reject the whole delta over. An unrecognised mode is deliberately left
alone: BusinessHourMode::Other exists so a mode added later still round
trips, and guessing its arity would turn that into a hard failure.

Finally, BusinessProfile's business_hours and categories are typed
BusinessHours and BusinessCategory, neither of which was re-exported, so
naming a result type in a signature still meant depending on wacore.
Both now come through the feature surface and the crate root.
@greptile-apps
greptile-apps Bot dismissed their stale review August 8, 2026 06:12

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

jlucaso1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

All five findings from the second Codex pass check out against the whatspec IR at WA 2.3000.1044659339 — fixed in cb899ea. Two of them were dropping data the response actually carries, which is the worst kind of gap here because nothing fails loudly.

Order variant properties. products[].variant_info.variant_properties[] {name, value} is real and I was discarding it. This is the most consequential of the five: every variant of one listing shares an id, name and price, so without the chosen size or colour an order for a variant product cannot be fulfilled at all — the caller gets a line item that looks complete and isn't.

Collection moderation status. Also real, and the asymmetry is the interesting part. A collection's status_info carries {status, can_appeal, reject_reason, commerce_url}; a product's carries only {status, can_appeal}. I was keeping just status on collections. reject_reason and commerce_url now come through, and Product correctly gains no counterpart — the fields don't exist there, so the types stay faithful to the shape rather than symmetric for its own sake.

Non-object order line item. Same class as the collections case fixed earlier; parse_order was the one parser still indexing before validating shape. Now a typed error.

Mode/time pairing. Confirmed in the bundle from both directions — the writer emits times iff a day has a range (if(r) for(i of r){...} else push({day_of_week, mode})), and the reader only keeps them for SPECIFIC_HOURS. So a ranged open_24h, or a specific_hours day with no range, is a stanza WA Web never produces and the server can reject the whole delta over.

One deliberate limit: an unrecognised mode is left alone rather than validated. BusinessHourMode::Other exists so a mode WhatsApp adds later still round-trips, and guessing its arity would convert that passthrough into a hard failure. There's a test pinning that.

Nested profile re-exports. Correct — BusinessProfile::business_hours and ::categories are typed BusinessHours and BusinessCategory, so naming a result type in a signature still required the wacore dependency the re-export block exists to avoid. Both now come through features::business and the crate root, along with the new VariantProperty.

Verified: 1394 + 1492 lib tests (6 new), doctests, clippy --all-features --all-targets, fmt --check — all clean.


Generated by Claude Code

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026
greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/business.rs`:
- Around line 1491-1524: Update the malformed-shape assertions in
malformed_nested_objects_are_errors to include distinct failure messages
identifying each catalog, collection, and order contract, including paging,
media, status_info, compliance_info, nested images, and variant_info cases.
Preserve the existing inputs and expected is_err/is_ok behavior while ensuring a
failed assertion names the malformed field or parser path.
- Around line 737-753: Change parse_importer_address to return
Option<ImporterAddress> directly, preserving its existing None-for-empty and
Some-for-populated behavior. Update its call site to remove the ? operator when
obtaining this value, while leaving object() responsible for error handling.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5d0f8901-b008-4ca0-a1e8-0582689ada4c

📥 Commits

Reviewing files that changed from the base of the PR and between dd4bbb3 and 5e62e2e.

📒 Files selected for processing (3)
  • src/features/business.rs
  • src/features/mod.rs
  • src/lib.rs

Comment thread src/features/business.rs Outdated
Comment thread src/features/business.rs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/features/business.rs Outdated
Comment thread src/features/business.rs Outdated
Both raised independently by CodeRabbit and cubic on the previous commit.

parse_importer_address kept a Result after the object guard moved into
`object()`, so every path returned Ok and the `?` at the call site
signalled a failure mode that no longer existed. It returns Option now.

The malformed-shape test packed nine contracts into bare is_err()
assertions, so a regression reported a line number and nothing else. The
catalog and order cases are table-driven with the field named in the
assertion message, and the importer_address shape — previously only
covered indirectly — gets its own case.
@greptile-apps
greptile-apps Bot dismissed their stale review August 8, 2026 07:11

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a88b82750f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/features/business.rs Outdated
Comment on lines +654 to +657
let sale_price = parse_price(&sale["price"], currency.as_deref()).map(|price| SalePrice {
price,
start_date: opt_str(&sale["start_date"]),
end_date: opt_str(&sale["end_date"]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pair sale window endpoints

When a response contains only start_date or only end_date, this parser exposes a half-populated window even though SalePrice documents that WhatsApp Web considers the window set only when both endpoints are present. Consumers can consequently schedule or display a promotion using a period the official client treats as unset; normalize both fields to None unless both parse successfully, or reject the partial shape as malformed.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

SalePrice documented that WhatsApp Web treats the window as set only when
both dates are present, but the parser surfaced each date independently,
so a response carrying one of them produced a half window the doc said
could not exist. The doc was right and the code was not.

The bundle settles it in both transports: the MEX reader is
`start_date != null && end_date != null ? {sale_start_date, sale_end_date}
: null`, and the IQ reader gates the same pair on
`hasChild("start_date") && hasChild("end_date")`. Either way a lone
endpoint is discarded rather than carried, so normalising it away here
mirrors the client instead of inventing a period it would not honour.

The sale price itself is unaffected — only the window is dropped.
@greptile-apps
greptile-apps Bot dismissed their stale review August 8, 2026 07:25

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

jlucaso1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Re: Codex's "Pair sale window endpoints" on src/features/business.rs — valid, fixed in 3a71b7b.

This one was a straight contradiction between my own doc and my own code. SalePrice stated that WhatsApp Web treats the window as set only when both ends are present, and the parser then surfaced each date independently — so it could produce exactly the half window the doc said could not exist.

I went back to the bundle rather than trusting either side of that contradiction, and it settles it identically in both transports:

  • MEX: start_date != null && end_date != null ? {sale_start_date, sale_end_date} : null
  • IQ: hasChild("start_date") && hasChild("end_date") ? {...} : null

The doc was right; the code was wrong. A lone endpoint is discarded by the official client, which makes normalising it away here mirroring rather than inventing — the same distinction I've applied throughout on what to keep versus what to drop, and the reason this one goes the opposite way from the importer and variant findings, where the server really did send data I was throwing out.

Scope worth being precise about: only the window is dropped, never the price. A response with a sale price and one date still yields the discounted price; it just doesn't claim a period. The test covers both directions and asserts the both-present case survives untouched.

Verified: 1395 + 1497 lib tests, clippy --all-features --all-targets, fmt --check — all clean.


Generated by Claude Code

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

Same additive collision as the previous merge, now with the bot-list and
new-chat-capping work upstream: both sides register a module in
src/features/mod.rs and append to the sorted re-export lists there and in
src/lib.rs. Resolved as the union throughout, so neither the business
surface nor upstream's Bot* and NewChatMessageCapping exports are lost.
@greptile-apps
greptile-apps Bot dismissed their stale review August 8, 2026 08:37

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ff9126247

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/features/business.rs
retailer_id: opt_str(&value["retailer_id"]),
name: opt_str(&value["name"]),
description: opt_str(&value["description"]),
url: opt_str(&value["url"]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve shimmed product URLs

When a catalog or collection product supplies only shimmed_url, this conversion exposes Product::url as None, even though both vendored response schemas include shimmed_url separately from url (wacore/src/iq/mex_operations.rs:7153-7158 and 7741-7747). Preserve that field or use it as the fallback so callers do not lose the server-provided product link.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

Comment thread src/features/business.rs
Comment on lines +338 to +339
/// Products returned inline per collection.
pub item_limit: u32,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expose paging for products within a collection

When a collection contains more products than item_limit (51 by default), get_collections returns only that inline prefix and exposes no cursor or follow-up operation for retrieving the remainder, so callers can silently treat a truncated collection as complete. The vendored query_product_single_collection operation accepts an after cursor and returns its own paging cursor (wacore/src/iq/mex_operations.rs:8066-8087 and 8338-8355); expose that paginated path or otherwise make truncation observable.

AGENTS.md reference: AGENTS.md:L5-L5

Useful? React with 👍 / 👎.

…cation

Two findings on the merged branch.

shimmed_url is sent alongside url rather than instead of it, arrives with
no opt-in, and was being dropped — so a product whose link the server
shimmed lost that form entirely. Both are surfaced as received. Using one
as a fallback for the other was tempting but would bake in a preference
the wire does not express: the shim adds interstitial handling, and which
to open is the caller's policy call.

The collections query returns only the first item_limit products of each
collection and carries no per-collection cursor, so a large collection
comes back truncated with nothing in the response marking it. That is the
same shape of trap as the collections cursor fixed earlier — a list that
looks complete and is not. Reading one to the end needs a different
operation, WAWebQueryProductSingleCollectionQuery, which is vendored with
its own after cursor but not wired up here; until it is, the length of
the list against item_limit is the only truncation signal, and the type
now says so rather than leaving callers to discover it.
@greptile-apps
greptile-apps Bot dismissed their stale review August 8, 2026 08:55

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

jlucaso1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Both Codex findings on the merge check out — handled differently, and the difference is the point.

shimmed_url — fixed in 40af422. Real loss: it's sent alongside url, not instead of it, arrives with no opt-in, and I was discarding it. Both forms are now surfaced as received.

I did not take the "use it as a fallback for url" half of the suggestion. That would bake in a preference the wire doesn't express — the shim adds interstitial handling, and WA Web picks per surface, so which to open is the caller's policy call, not something to guess in a parser. Same line I've drawn all session: expose what the server sent, don't invent semantics on top.

Collection product truncation — documented, not implemented. The finding is correct and the trap is real: get_collections returns only the first item_limit products per collection, and the response carries no per-collection cursor, so a large collection comes back looking complete. That's the same shape as the collections-cursor bug fixed earlier.

But the remedy isn't a parser fix — it's a different operation. WAWebQueryProductSingleCollectionQuery (doc id 9546992575408789) is vendored with its own after cursor and is what reads a collection to the end. Wiring it up is a new public method, which is the maintainer's scope call, not something to slip into a review round.

What I could close cheaply, I did: Collection::products now states that the list can be truncated, that products.len() == item_limit is the only available signal, and names the operation that fixes it. A caller can now detect truncation instead of discovering it in production.

Verified: 1413 + 1511 lib tests (33 in the business feature), clippy --all-features --all-targets, fmt --check — all clean.


Generated by Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: This PR adds a new public business API surface (catalog, collections, order, and profile mutations) and refreshes vendored MEX operations. The protocol choices, response-shape contracts, and mutating profile behavior are product/API decisions a human should sign off.

Re-trigger cubic

jlucaso1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Correction on Semver Checks — it now has two independent causes, and only one is this PR's.

wacore — 5 breaks, mine. The vendored MEX refresh, required to fix the stale query_catalog doc id. Unchanged from what I reported earlier.

wacore-binary — 1 break, not mine. New as of the merge:

enum_variant_added: variant BinaryError::UnexpectedFormatByte
  wacore/binary/src/error.rs:30

That variant came from 28710ff"fix(binary): make a received stanza sendable as it stands (#1259)". It is on main, and this branch has zero commits touching wacore/binary/; it reached this PR purely through merging main.

Two things follow that are worth knowing before the version decision:

  1. wacore-binary passed this check on every earlier run of this PR and fails only now, so main is currently carrying an unflagged breaking change to a public exhaustive enum. BinaryError has no #[non_exhaustive], which is why adding a variant trips the lint — probably worth raising on fix(binary): make a received stanza sendable as it stands #1259 rather than discovering it at release time.
  2. A 0.7.0 → 0.8.0 bump would now cover both, but the two breaks are unrelated in origin, and attributing all of them to the business PR would be wrong.

I've deliberately left the upstream one alone — fixing another PR's break inside a feature branch isn't mine to do, and silently adding #[non_exhaustive] to BinaryError would be a public API decision of its own.


Generated by Claude Code

@jlucaso1
jlucaso1 merged commit 64adf5b into main Aug 8, 2026
25 of 26 checks passed
@jlucaso1
jlucaso1 deleted the claude/business-catalog-products-orders-0mb890 branch August 8, 2026 12:14
jlucaso1 pushed a commit that referenced this pull request Aug 8, 2026
Third merge with the same conflict in the same place: the
`pub use features::{…}` list in `src/lib.rs`. rustfmt re-wraps the whole
block, so any two additions collide regardless of where they land. This
branch adds `AppStateSettings` and `QuickReplies`; main adds the business
profile and catalog surface from #1252. Took the union.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Up7G7AwUp1q8KqaGCAugJy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-design breaking-change size-increase-ok Accepted binary-size increase: downgrades the per-PR size gate to a warning

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants