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.

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.

ColumnTypeNullDefaultReferences · on update / on deleteHolds
idmeaningINTEGER, auto-incrementno—primary keyThe bundle. Existing ids are kept, so PAs and action feeds that point at a thread now point at its bundle.
marketplace_iddroppedINTEGERno—marketplace.id · no ruleDropped in M2. A bundle’s listings are its items; every existing row’s listing is copied into the items table first.
vendor_idINTEGERno—accounts.id · no ruleThe buyer.
account_idINTEGERno—accounts.id · no ruleThe seller facility. Every item’s listing belongs to it.
action_bymeaningINTEGERno—accounts.id · no ruleThe account that acted last on the bundle.
counter_offer_idmeaningINTEGERno—marketplace_counter_offers.id · no ruleThe bundle’s current offer row.
vendor_counter_offer_idmeaningINTEGERyes—marketplace_counter_offers.id · no ruleThe vendor’s last offer on the bundle.
facility_counter_offer_idmeaningINTEGERyes—marketplace_counter_offers.id · no ruleThe facility’s last offer on the bundle.
created_attimestamptznonow()—When the offer was sent.
updated_attimestamptznonow()—
deleted_attimestamptzyes——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.

ColumnTypeNullDefaultReferences · on update / on deleteHolds
idINTEGER, auto-incrementno—primary keyThe item.
marketplace_offer_association_idINTEGERno—marketplace_offer_associations.id · CASCADE / RESTRICTThe bundle.
marketplace_idINTEGERno—marketplace.id · no rule, like the other listing keysThe listing: always a parent listing. Its component listings come along, as they do today.
created_attimestamptznonow()—
updated_attimestamptznonow()—
deleted_attimestamptzyes——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.

ColumnTypeNullDefaultReferences · on update / on deleteHolds
idINTEGER, auto-incrementno—primary keyThe offer row.
marketplace_offer_association_idnewINTEGERno · after M2—marketplace_offer_associations.id · CASCADE / RESTRICTThe bundle this step belongs to.
marketplace_iddroppedINTEGERno—marketplace.id · no ruleDropped in M2. A step belongs to the bundle, not to one listing.
account_idINTEGERno—accounts.id · CASCADE / RESTRICTThe seller, the same as the bundle’s. Kept: the access checks and the history read it.
vendor_idINTEGERno—accounts.id · CASCADE / RESTRICTThe vendor, the same as the bundle’s. Kept for the same reason.
action_byINTEGERno—accounts.id · CASCADE / SET NULLWho made this offer. SET NULL on a column that cannot be null, so deleting that account is refused.
statusmeaningenum_marketplace_counter_offers_statusno——The status of the whole bundle. See status values.
amountmeaningFLOAT(12,2) · Postgres keeps it as double precisionno——The bundle total at this step — the one number both sides see.
created_byINTEGERno—users.id · no ruleThe user who acted.
previous_statusenum_marketplace_counter_offers_statusyes——The status before Awaiting Signature, put back if the PA fails to generate.
declined_reasonJSONByes——Unused today. It can record which listing closed the bundle when that listing went elsewhere.
created_attimestamptzyesnow()—
updated_attimestamptzyesnow()—
deleted_attimestamptzyes——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.

ColumnTodayAfter
request_typepa for a marketplace PA.Unchanged.
marketplace_iddroppedThe 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_idmeaningThe 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, attachmentsThe DocuSign envelope and its signers.One envelope for the whole bundle, listing every item.
capex_idThe PA number, set by a trigger on insert.One number per bundle.
transaction_idSet on completion.The bundle’s transaction.
item_idsCopilot 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.

ValueWritten today byMeans
Awaiting Responsesend; the vendor’s counterThe facility must reply.
Offer Receivedthe facility’s counterThe vendor must reply.
Offer RejectedrejectThe latest offer was turned down.
Awaiting Signatureaccept; PA resendThe PA is out for signature. Locks every listing in the bundle.
PA RejectedDocuSign decline or void (import role)Still locks them; the PA can be sent again.
Logistics RequiredcompletionThe winning bundle; a transaction exists.
Soldaccept and completion, for competing offers; a listing sold elsewhereThis offer lost one of its listings, which closes the whole bundle (Q2).
Canceledcancel by the vendor or an adminWithdrawn. The vendor may offer on the listings again.
Removed From Marketplacelisting or vendor account removedClosed.
Offer Accepted · Offer Declined · Shortlisted · PA Required · ArchivednothingOlder 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

ColumnTodayIn a bundle
parent_idSet on component listings; offers exist only on parent listings.Items are parent listings only. Their components come along, as today.
account_idThe seller facility.The same for every item, and equal to the bundle’s account_id.
status, market_place_flag, time_limitDecide whether a listing can take an offer.Checked for each item’s listing, exactly as today.
fair_market_valueThe listing’s FMV.Weights the listing’s line price at completion (Q1).
counter_offer_statusRolled 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_idThe listing’s active PA.Every item’s listing points at the bundle’s PA while it is active.
transaction_idSet on completion.Every item’s listing gets the bundle’s one transaction.

transaction_equipment_detailsthe transaction’s lines

ColumnTodayIn a bundle
transaction_idThe order.One transaction per bundle.
marketplace_id, inventory_idOne line for the listing.One line per item.
unit_price, total_price, quantityThe listing’s line carries the full offer amount.Each item’s line carries its part of the total, split at completion (Q1).
parent_idComponent 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.

NameOnKindWhy
uq_marketplace_offer_items_bundle_listingmarketplace_offer_items (marketplace_offer_association_id, marketplace_id)uniqueA listing appears once in a bundle.
idx_marketplace_offer_items_marketplace_idmarketplace_offer_items (marketplace_id)indexEvery read that starts from a listing.
idx_marketplace_counter_offers_association_id_idmarketplace_counter_offers (marketplace_offer_association_id, id)indexA bundle’s history, in order.
idx_marketplace_offer_associations_vendor_idmarketplace_offer_associations (vendor_id)indexA vendor’s bundles: My Offers and the send check.
new foreign keysitems → bundles and → listings; offer rows → bundlesCASCADE / RESTRICT; the listing key with no ruleA bundle cannot be deleted while an item or an offer row still uses it.
dropped foreign keysthe three listing columnsdropped in M2They 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

  1. 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
  2. R2A bundle’s items are written once, at send, and never change. A new offer after a bundle ends is a new bundle.items
  3. R3Every offer row belongs to exactly one bundle, and the bundle’s pointers always name its latest rows.every step
  4. R4While a bundle’s PA is active, every item’s listing points at it.PA
  5. R5Each step changes the bundle in one database transaction.every step

Entity changesbackendApi · src/shared/entity