Schema · one new table, three changed · nothing applied
Tables & changes
Every column of the offer thread that becomes the bundle, the new items table and the offer rows, then the PA’s one change and the tables that stay as they are. The current definitions come from the migrations that created and altered each table and from the API’s entities, not from a dump of a live database.
- new
- meaning changes
- dropped in M2
- unchanged
marketplace_offer_associationschanged · becomes the bundle · one row per offer
Today this is the thread between one vendor and one listing. It becomes the bundle: one row per offer, between one vendor and one seller facility, over the listings in its items. No column is added. Every existing row is already a bundle of one, and its listing moves to the items table. The table keeps its name, since renaming it would touch every query for no change in behaviour.
| Column | Type | Null | Default | References · on update / on delete | Holds |
|---|---|---|---|---|---|
| idmeaning | INTEGER, auto-increment | no | — | primary key | The bundle. Existing ids are kept, so PAs and action feeds that point at a thread now point at its bundle. |
| marketplace_iddropped | INTEGER | no | — | marketplace.id · no rule | Dropped in M2. A bundle’s listings are its items; every existing row’s listing is copied into the items table first. |
| vendor_id | INTEGER | no | — | accounts.id · no rule | The buyer. |
| account_id | INTEGER | no | — | accounts.id · no rule | The seller facility. Every item’s listing belongs to it. |
| action_bymeaning | INTEGER | no | — | accounts.id · no rule | The account that acted last on the bundle. |
| counter_offer_idmeaning | INTEGER | no | — | marketplace_counter_offers.id · no rule | The bundle’s current offer row. |
| vendor_counter_offer_idmeaning | INTEGER | yes | — | marketplace_counter_offers.id · no rule | The vendor’s last offer on the bundle. |
| facility_counter_offer_idmeaning | INTEGER | yes | — | marketplace_counter_offers.id · no rule | The facility’s last offer on the bundle. |
| created_at | timestamptz | no | now() | — | When the offer was sent. |
| updated_at | timestamptz | no | now() | — | |
| deleted_at | timestamptz | yes | — | — | Soft delete, as today. The divestiture-to-auction cron sets it. |
marketplace_offer_itemsnew · one row per listing in a bundle
An item is only the link between a bundle and a listing. The listing’s details — inventory, model, FMV, condition — stay on the listing, so nothing is copied that could drift. Items are written once, when the offer is sent, and never change.
| Column | Type | Null | Default | References · on update / on delete | Holds |
|---|---|---|---|---|---|
| id | INTEGER, auto-increment | no | — | primary key | The item. |
| marketplace_offer_association_id | INTEGER | no | — | marketplace_offer_associations.id · CASCADE / RESTRICT | The bundle. |
| marketplace_id | INTEGER | no | — | marketplace.id · no rule, like the other listing keys | The listing: always a parent listing. Its component listings come along, as they do today. |
| created_at | timestamptz | no | now() | — | |
| updated_at | timestamptz | no | now() | — | |
| deleted_at | timestamptz | yes | — | — | Soft delete, set only together with its bundle. |
marketplace_counter_offerschanged · one row per bundle per step
One row for each offer or counter-offer on a bundle, whatever its size, bound to the bundle and never to an item. Every existing row already is one: a single-listing offer is a bundle of one, so its rows keep their values and only gain the bundle link.
| Column | Type | Null | Default | References · on update / on delete | Holds |
|---|---|---|---|---|---|
| id | INTEGER, auto-increment | no | — | primary key | The offer row. |
| marketplace_offer_association_idnew | INTEGER | no · after M2 | — | marketplace_offer_associations.id · CASCADE / RESTRICT | The bundle this step belongs to. |
| marketplace_iddropped | INTEGER | no | — | marketplace.id · no rule | Dropped in M2. A step belongs to the bundle, not to one listing. |
| account_id | INTEGER | no | — | accounts.id · CASCADE / RESTRICT | The seller, the same as the bundle’s. Kept: the access checks and the history read it. |
| vendor_id | INTEGER | no | — | accounts.id · CASCADE / RESTRICT | The vendor, the same as the bundle’s. Kept for the same reason. |
| action_by | INTEGER | no | — | accounts.id · CASCADE / SET NULL | Who made this offer. SET NULL on a column that cannot be null, so deleting that account is refused. |
| statusmeaning | enum_marketplace_counter_offers_status | no | — | — | The status of the whole bundle. See status values. |
| amountmeaning | FLOAT(12,2) · Postgres keeps it as double precision | no | — | — | The bundle total at this step — the one number both sides see. |
| created_by | INTEGER | no | — | users.id · no rule | The user who acted. |
| previous_status | enum_marketplace_counter_offers_status | yes | — | — | The status before Awaiting Signature, put back if the PA fails to generate. |
| declined_reason | JSONB | yes | — | — | Unused today. It can record which listing closed the bundle when that listing went elsewhere. |
| created_at | timestamptz | yes | now() | — | |
| updated_at | timestamptz | yes | now() | — | |
| deleted_at | timestamptz | yes | — | — | Soft delete. |
docusign_requestschanged · the PA’s listing column goes
The columns a marketplace PA uses. The copilot request columns in the same table are unchanged and not listed. No column is added: the PA’s accepted offer row already carries the bundle.
| Column | Today | After |
|---|---|---|
| request_type | pa for a marketplace PA. | Unchanged. |
| marketplace_iddropped | The listing the PA is for. Optional, since copilot requests never set it. | Dropped in M2. A PA’s listings are its bundle’s items, reached through its accepted offer row. |
| marketplace_counter_offer_idmeaning | The accepted offer row. | The bundle’s accepted row. Its bundle link names the bundle, and through it the listings. A PA outcome — rejected, resent, completed — is one update to that row. |
| envelope_id, approvers, attachments | The DocuSign envelope and its signers. | One envelope for the whole bundle, listing every item. |
| capex_id | The PA number, set by a trigger on insert. | One number per bundle. |
| transaction_id | Set on completion. | The bundle’s transaction. |
| item_ids | Copilot pre-order items, with a GIN index. | Left alone: bundles do not use it. |
Status valuesenum_marketplace_counter_offers_status · no change
Fourteen values, and no new one. The status on a bundle’s current offer row is the status of every listing in it, so each transition below is one row update.
| Value | Written today by | Means |
|---|---|---|
| Awaiting Response | send; the vendor’s counter | The facility must reply. |
| Offer Received | the facility’s counter | The vendor must reply. |
| Offer Rejected | reject | The latest offer was turned down. |
| Awaiting Signature | accept; PA resend | The PA is out for signature. Locks every listing in the bundle. |
| PA Rejected | DocuSign decline or void (import role) | Still locks them; the PA can be sent again. |
| Logistics Required | completion | The winning bundle; a transaction exists. |
| Sold | accept and completion, for competing offers; a listing sold elsewhere | This offer lost one of its listings, which closes the whole bundle (Q2). |
| Canceled | cancel by the vendor or an admin | Withdrawn. The vendor may offer on the listings again. |
| Removed From Marketplace | listing or vendor account removed | Closed. |
| Offer Accepted · Offer Declined · Shortlisted · PA Required · Archived | nothing | Older values with no writer in the current code. |
Tables that stay as they areno column changes
These are the columns a bundle relies on, and what it does with each.
marketplacethe listing
| Column | Today | In a bundle |
|---|---|---|
| parent_id | Set on component listings; offers exist only on parent listings. | Items are parent listings only. Their components come along, as today. |
| account_id | The seller facility. | The same for every item, and equal to the bundle’s account_id. |
| status, market_place_flag, time_limit | Decide whether a listing can take an offer. | Checked for each item’s listing, exactly as today. |
| fair_market_value | The listing’s FMV. | Weights the listing’s line price at completion (Q1). |
| counter_offer_status | Rolled up from the listing’s threads. | Rolled up through the listing’s items: for each bundle it is in, that bundle’s current offer row. |
| docusign_request_id | The listing’s active PA. | Every item’s listing points at the bundle’s PA while it is active. |
| transaction_id | Set on completion. | Every item’s listing gets the bundle’s one transaction. |
transaction_equipment_detailsthe transaction’s lines
| Column | Today | In a bundle |
|---|---|---|
| transaction_id | The order. | One transaction per bundle. |
| marketplace_id, inventory_id | One line for the listing. | One line per item. |
| unit_price, total_price, quantity | The listing’s line carries the full offer amount. | Each item’s line carries its part of the total, split at completion (Q1). |
| parent_id | Component lines point at their listing’s line, without a price. | Unchanged. |
action_feedsvendor cards
marketplace_offer_association_id ties each vendor card to a thread. The thread is now the bundle, so a vendor gets one card per bundle instead of one per listing, with no column change. The card’s link to a listing moves to the bundle’s items.
Indexes and constraintsadded in M1 · dropped in M2
The offer tables have no index today beyond their primary keys.
| Name | On | Kind | Why |
|---|---|---|---|
| uq_marketplace_offer_items_bundle_listing | marketplace_offer_items (marketplace_offer_association_id, marketplace_id) | unique | A listing appears once in a bundle. |
| idx_marketplace_offer_items_marketplace_id | marketplace_offer_items (marketplace_id) | index | Every read that starts from a listing. |
| idx_marketplace_counter_offers_association_id_id | marketplace_counter_offers (marketplace_offer_association_id, id) | index | A bundle’s history, in order. |
| idx_marketplace_offer_associations_vendor_id | marketplace_offer_associations (vendor_id) | index | A vendor’s bundles: My Offers and the send check. |
| new foreign keys | items → bundles and → listings; offer rows → bundles | CASCADE / RESTRICT; the listing key with no rule | A bundle cannot be deleted while an item or an offer row still uses it. |
| dropped foreign keys | the three listing columns | dropped in M2 | They go with their columns. |
No unique rule for “one live bundle per listing per vendor”: the listing is on the item, the vendor on the bundle and whether it is live on the offer row, so no single index spans them. Send and accept lock the listing rows and check it in code, which is also the only place it is enforced today.
Rules the code enforcesnot expressible in the schema
- R1Every listing in a bundle belongs to the bundle’s seller, has no
parent_id, passes the one-listing checks (flag and status, the divestiture and auction rule, not locked by another PA), and is in no live bundle of this vendor — checked with the listing rows locked.send - R2A bundle’s items are written once, at send, and never change. A new offer after a bundle ends is a new bundle.items
- R3Every offer row belongs to exactly one bundle, and the bundle’s pointers always name its latest rows.every step
- R4While a bundle’s PA is active, every item’s listing points at it.PA
- R5Each step changes the bundle in one database transaction.every step
Entity changesbackendApi · src/shared/entity
- New
MarketplaceOfferItems(soft delete), exported from the entity index and registered in the models list, linked to its bundle and its listing. MarketplaceOfferAssociationslosesmarketplaceIdand its link to the listing, and gains its items.MarketplaceCounterOffersgainsmarketplaceOfferAssociationIdand a link to its bundle, and losesmarketplaceIdand its link to the listing (marketPlaceData).DocusignRequestslosesmarketplaceIdand its link to the listing (marketplaceDetails).MarketPlace: its link to offer threads (offerAssociations) becomes a link to offer items.- A removed model link fails at run time, not at compile time, for any include that still names it. Search for each one —
offerAssociations,marketPlaceData,marketplaceDetails— before removing it.