@stacksjs/commerce 0.70.380 → 0.71.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auctions/bids/fetch.d.ts +21 -0
- package/dist/auctions/bids/index.d.ts +2 -0
- package/dist/auctions/bids/place.d.ts +15 -0
- package/dist/auctions/engine/anti-snipe.d.ts +22 -0
- package/dist/auctions/engine/increments.d.ts +29 -0
- package/dist/auctions/engine/index.d.ts +14 -0
- package/dist/auctions/engine/proxy.d.ts +27 -0
- package/dist/auctions/engine/winners.d.ts +22 -0
- package/dist/auctions/index.d.ts +37 -0
- package/dist/auctions/lots/fetch.d.ts +38 -0
- package/dist/auctions/lots/index.d.ts +3 -0
- package/dist/auctions/lots/lifecycle.d.ts +42 -0
- package/dist/auctions/notifications.d.ts +34 -0
- package/dist/auctions/pledges/index.d.ts +27 -0
- package/dist/auctions/realtime.d.ts +20 -0
- package/dist/auctions/rules.d.ts +17 -0
- package/dist/auctions/types.d.ts +168 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -5
- package/package.json +2 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BidRow } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* The bid currently winning a lot, or null when nobody has bid.
|
|
4
|
+
*
|
|
5
|
+
* There is exactly one `leading` row per open lot; `placeBid` maintains that
|
|
6
|
+
* invariant inside a transaction. `orderBy` is still spelled out so a stray
|
|
7
|
+
* second row (a hand-edited record, a restored backup) resolves to the highest
|
|
8
|
+
* one rather than to whatever the storage engine returns first.
|
|
9
|
+
*/
|
|
10
|
+
export declare function leadingBid(itemId: number): Promise<BidRow | null>;
|
|
11
|
+
/** Every bid on a lot, newest first - the lot's activity feed. */
|
|
12
|
+
export declare function bidsForItem(itemId: number): Promise<BidRow[]>;
|
|
13
|
+
/** Every bid in an auction, oldest first - the settlement input. */
|
|
14
|
+
export declare function bidsForAuction(auctionId: number): Promise<BidRow[]>;
|
|
15
|
+
/**
|
|
16
|
+
* One bidder's bids across an auction, for the "your bids" panel a parent
|
|
17
|
+
* refreshes all night and for addressing outbid notices.
|
|
18
|
+
*/
|
|
19
|
+
export declare function bidsByBidder(auctionId: number, bidderEmail: string): Promise<BidRow[]>;
|
|
20
|
+
/** Distinct bidders in an auction, for audience resolution on notifications. */
|
|
21
|
+
export declare function biddersFor(auctionId: number): Promise<{ name: string, email: string }[]>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BidRequest, PlacedBid } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Place a bid.
|
|
4
|
+
*
|
|
5
|
+
* The decision of what should happen is the pure engine's
|
|
6
|
+
* (`resolveBid`); everything here is the consequences: which rows change, what
|
|
7
|
+
* the lot's close time becomes, and who needs to be told. Keeping the two apart
|
|
8
|
+
* is what lets a bidding war be tested without a database and a database write
|
|
9
|
+
* be reviewed without re-deriving the auction rules.
|
|
10
|
+
*
|
|
11
|
+
* Every write runs in one transaction. A silent auction's worst failure mode is
|
|
12
|
+
* two `leading` bids on one lot - two people are told they won the same
|
|
13
|
+
* vacation package, and someone has to call one of them back.
|
|
14
|
+
*/
|
|
15
|
+
export declare function placeBid(request: BidRequest): Promise<PlacedBid>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AuctionItemRow, AuctionRules } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a bid landing at `now` should push this lot's close out, and to when.
|
|
4
|
+
*
|
|
5
|
+
* Sniping is the reason paper bid sheets get guarded by a parent with a
|
|
6
|
+
* stopwatch: a bid dropped in the last four seconds wins not because it was the
|
|
7
|
+
* highest anyone would pay, but because nobody could answer it. Extending the
|
|
8
|
+
* close whenever a bid lands inside the window converts the last minute back
|
|
9
|
+
* into an auction - the lot ends when bidding actually stops.
|
|
10
|
+
*
|
|
11
|
+
* `max_extensions` is the counterweight. Without it two determined bidders can
|
|
12
|
+
* hold one lot open indefinitely, and the gala staff cannot go home. Once a lot
|
|
13
|
+
* has been extended that many times it closes on schedule, and the bidding war
|
|
14
|
+
* is settled by whoever was ahead.
|
|
15
|
+
*
|
|
16
|
+
* Returns null when nothing should change.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extendedCloseAt(item: Pick<AuctionItemRow, 'closes_at' | 'extension_count'>, closesAt: Date, rules: Pick<AuctionRules, 'antiSnipeMinutes' | 'extendOnBidWindowMinutes' | 'maxExtensions'>, now: Date): Date | null;
|
|
19
|
+
/**
|
|
20
|
+
* The close time in force for a lot: its own override, else the auction's.
|
|
21
|
+
*/
|
|
22
|
+
export declare function effectiveCloseAt(item: Pick<AuctionItemRow, 'closes_at'>, auctionClosesAt: string | Date): Date;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AuctionItemRow, IncrementTier } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* The increment that applies at `amount`.
|
|
4
|
+
*
|
|
5
|
+
* A lot's own `min_increment` overrides the ladder entirely - that is the
|
|
6
|
+
* escape hatch for the lot that wants round hundreds regardless of where the
|
|
7
|
+
* bidding starts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function incrementFor(amount: number, ladder?: IncrementTier[], itemIncrement?: number | null): number;
|
|
10
|
+
/**
|
|
11
|
+
* The smallest bid that can be placed on this lot right now.
|
|
12
|
+
*
|
|
13
|
+
* With no bids yet that is the starting bid itself, not the starting bid plus
|
|
14
|
+
* an increment: the first bidder should be able to type the number printed on
|
|
15
|
+
* the bid sheet.
|
|
16
|
+
*/
|
|
17
|
+
export declare function nextMinimumBid(item: Pick<AuctionItemRow, 'starting_bid' | 'min_increment'>, leadingAmount: number | null, ladder?: IncrementTier[]): number;
|
|
18
|
+
/**
|
|
19
|
+
* The default increment ladder, in cents.
|
|
20
|
+
*
|
|
21
|
+
* A flat increment is wrong at both ends of a gala catalogue: $5 steps turn a
|
|
22
|
+
* $4,000 vacation package into a hundred-bid slog, and $100 steps price
|
|
23
|
+
* everyone out of the $60 class art project. The ladder is what a live
|
|
24
|
+
* auctioneer does by instinct - bigger money moves in bigger steps.
|
|
25
|
+
*
|
|
26
|
+
* Tiers are read in order and the first whose `upTo` exceeds the current
|
|
27
|
+
* amount wins, so they must stay sorted ascending with a single `null` last.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DEFAULT_INCREMENT_LADDER: IncrementTier[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { IncomingBid, LeadingBid } from './proxy';
|
|
2
|
+
/**
|
|
3
|
+
* The pure auction engine.
|
|
4
|
+
*
|
|
5
|
+
* Nothing in here reads a clock, a config file or a database - every input is
|
|
6
|
+
* an argument. That is what lets the interesting rules (a proxy-bid war, a tie,
|
|
7
|
+
* an anti-snipe extension at the boundary, a reserve that was not met) be
|
|
8
|
+
* tested exhaustively in milliseconds, and it is why the persistence layer in
|
|
9
|
+
* `../bids` and `../auctions` stays as thin as it does.
|
|
10
|
+
*/
|
|
11
|
+
export { effectiveCloseAt, extendedCloseAt } from './anti-snipe';
|
|
12
|
+
export { DEFAULT_INCREMENT_LADDER, incrementFor, nextMinimumBid } from './increments';
|
|
13
|
+
export { resolveBid } from './proxy';
|
|
14
|
+
export { determineWinner, settle } from './winners';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AuctionItemRow, BidResolution, BidRow, IncrementTier } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve one incoming bid against the standing leader.
|
|
4
|
+
*
|
|
5
|
+
* This is proxy bidding, the model every online auction has converged on: a
|
|
6
|
+
* bidder states a ceiling, and the house bids on their behalf in increments
|
|
7
|
+
* only as far as it must. The visible number is therefore almost never the
|
|
8
|
+
* ceiling - it is one increment above whatever the losing side was willing to
|
|
9
|
+
* pay, which is exactly the price discovery a paper bid sheet cannot do.
|
|
10
|
+
*
|
|
11
|
+
* The function is pure. It reads no clock, touches no database, and returns
|
|
12
|
+
* what *should* happen; `placeBid` is what makes it so. That split is what
|
|
13
|
+
* makes the interesting cases - a ceiling war, a tie, a bidder raising their
|
|
14
|
+
* own maximum - testable without a gala.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveBid(item: Pick<AuctionItemRow, 'starting_bid' | 'min_increment' | 'buy_now_price'>, leader: LeadingBid, incoming: IncomingBid, ladder?: IncrementTier[]): BidResolution;
|
|
17
|
+
/**
|
|
18
|
+
* The bid a challenger is offering, reduced to what the engine needs.
|
|
19
|
+
*/
|
|
20
|
+
export declare interface IncomingBid {
|
|
21
|
+
bidderEmail: string
|
|
22
|
+
bidderName: string
|
|
23
|
+
amount: number
|
|
24
|
+
maxAmount?: number | null
|
|
25
|
+
}
|
|
26
|
+
/** The current leader, or null when nobody has bid on the lot yet. */
|
|
27
|
+
export type LeadingBid = Pick<BidRow, 'bidder_email' | 'bidder_name' | 'amount' | 'max_amount'> | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AuctionItemRow, AuctionSettlement, BidRow, ItemOutcome, PledgeRow } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Decide a single lot's outcome from its bids.
|
|
4
|
+
*
|
|
5
|
+
* Ordering is explicit rather than inherited from whatever the query returned:
|
|
6
|
+
* highest amount wins, and the earlier bid wins a tie. Two bids can legitimately
|
|
7
|
+
* share an amount when one bidder's proxy stepped up to exactly another's
|
|
8
|
+
* ceiling, and "first to commit" is the rule every bid sheet has always used.
|
|
9
|
+
*/
|
|
10
|
+
export declare function determineWinner(item: Pick<AuctionItemRow, 'id' | 'lot_number' | 'title' | 'reserve_price'>, bids: BidRow[]): ItemOutcome;
|
|
11
|
+
/**
|
|
12
|
+
* Roll a set of lot outcomes and pledges into the number the school actually
|
|
13
|
+
* cares about.
|
|
14
|
+
*
|
|
15
|
+
* `valueDelta` compares winning bids against fair market value. It is the
|
|
16
|
+
* honest read on a catalogue: a positive delta means donors paid over the value
|
|
17
|
+
* of what they took home, which is the point of a benefit auction; a negative
|
|
18
|
+
* one means the room got bargains and the procurement committee has a
|
|
19
|
+
* conversation to have. Lots with no stated value are excluded rather than
|
|
20
|
+
* counted as zero, which would make every catalogue look like a loss.
|
|
21
|
+
*/
|
|
22
|
+
export declare function settle(items: Pick<AuctionItemRow, 'id' | 'fair_market_value'>[], outcomes: ItemOutcome[], pledges: PledgeRow[], opts: { auctionId: number, currency: string, goalAmount?: number | null }): AuctionSettlement;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auctions.
|
|
3
|
+
*
|
|
4
|
+
* A benefit auction is commerce with the prices taken out: lots instead of
|
|
5
|
+
* products, bidders instead of customers, and a hard stop time instead of a
|
|
6
|
+
* checkout. It sits inside commerce because everything downstream of the
|
|
7
|
+
* hammer - what was sold, for how much, who owes it, what gets receipted - is
|
|
8
|
+
* ordinary commerce, and splitting the two would mean two vocabularies for one
|
|
9
|
+
* night's money.
|
|
10
|
+
*
|
|
11
|
+
* Three layers, deliberately separable:
|
|
12
|
+
*
|
|
13
|
+
* engine/ pure rules - increments, proxy bidding, anti-snipe, winners
|
|
14
|
+
* lots/ bids/ pledges/ persistence on top of those rules
|
|
15
|
+
* realtime, notifications how the room and the bidders find out
|
|
16
|
+
*
|
|
17
|
+
* Everything is in integer cents. See `./types` for the row shapes and why they
|
|
18
|
+
* are declared here rather than imported from the generated ORM types.
|
|
19
|
+
*
|
|
20
|
+
* Both spellings work, since a file that only places bids should not have to
|
|
21
|
+
* reach through two namespaces to do it:
|
|
22
|
+
*
|
|
23
|
+
* commerce.auctions.placeBid(...) flat, like every other commerce module
|
|
24
|
+
* commerce.auctions.bids.placeBid(...) grouped, when a file touches several layers
|
|
25
|
+
*/
|
|
26
|
+
export * from './bids/index';
|
|
27
|
+
export * as bids from './bids/index';
|
|
28
|
+
export * from './engine/index';
|
|
29
|
+
export * as engine from './engine/index';
|
|
30
|
+
export * from './lots/index';
|
|
31
|
+
export * as lots from './lots/index';
|
|
32
|
+
export * from './notifications';
|
|
33
|
+
export * from './pledges/index';
|
|
34
|
+
export * as pledges from './pledges/index';
|
|
35
|
+
export * from './realtime';
|
|
36
|
+
export * from './rules';
|
|
37
|
+
export * from './types';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { nextMinimumBid } from '../engine/increments';
|
|
2
|
+
import type { AuctionItemRow, AuctionRow } from '../types';
|
|
3
|
+
export declare function fetchAuction(id: number): Promise<AuctionRow | null>;
|
|
4
|
+
/** The auction attached to an event, which is how the public page finds it. */
|
|
5
|
+
export declare function fetchAuctionForEvent(eventId: number): Promise<AuctionRow | null>;
|
|
6
|
+
export declare function fetchItems(auctionId: number): Promise<AuctionItemRow[]>;
|
|
7
|
+
/**
|
|
8
|
+
* The catalogue as a bidder sees it: every lot with its current price, how
|
|
9
|
+
* contested it is, and the number to beat.
|
|
10
|
+
*
|
|
11
|
+
* One grouped query rather than a query per lot - a gala catalogue is a hundred
|
|
12
|
+
* lots and the page is opened by a few hundred phones at once, so the N+1 here
|
|
13
|
+
* would be the whole evening's load.
|
|
14
|
+
*
|
|
15
|
+
* Proxy ceilings are not selected. They must never reach a template.
|
|
16
|
+
*/
|
|
17
|
+
export declare function fetchCatalogue(auctionId: number): Promise<ItemWithBidState[]>;
|
|
18
|
+
/**
|
|
19
|
+
* The live numbers behind the gala monitor: what the room has committed so far,
|
|
20
|
+
* against the goal.
|
|
21
|
+
*/
|
|
22
|
+
export declare function auctionTotals(auctionId: number): Promise<AuctionTotals>;
|
|
23
|
+
export declare interface ItemWithBidState extends AuctionItemRow {
|
|
24
|
+
currentBid: number
|
|
25
|
+
bidCount: number
|
|
26
|
+
nextMinimumBid: number
|
|
27
|
+
leaderName: string | null
|
|
28
|
+
}
|
|
29
|
+
export declare interface AuctionTotals {
|
|
30
|
+
currentBidTotal: number
|
|
31
|
+
pledgeTotal: number
|
|
32
|
+
raised: number
|
|
33
|
+
goalAmount: number | null
|
|
34
|
+
bidCount: number
|
|
35
|
+
bidderCount: number
|
|
36
|
+
lotsOffered: number
|
|
37
|
+
lotsWithBids: number
|
|
38
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { AuctionTotals, ItemWithBidState } from './fetch';
|
|
2
|
+
export { auctionTotals, fetchAuction, fetchAuctionForEvent, fetchCatalogue, fetchItems } from './fetch';
|
|
3
|
+
export { closeAuction, closeDueItems, closeItem, closingSoon, openAuction, settleAuction } from './lifecycle';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AuctionItemRow, AuctionRow, AuctionSettlement, ItemOutcome } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Open an auction for bidding.
|
|
4
|
+
*
|
|
5
|
+
* Lots go open with it. A lot held back deliberately (a live-auction headline
|
|
6
|
+
* item that is only listed for display) keeps whatever status it already has if
|
|
7
|
+
* it is not `draft`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function openAuction(auctionId: number, now?: Date): Promise<AuctionRow | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Close one lot and decide it.
|
|
12
|
+
*
|
|
13
|
+
* Returns the outcome, or null when the lot was already resolved - which makes
|
|
14
|
+
* the function safe to call from a per-minute job that may overlap with an
|
|
15
|
+
* organizer clicking "close now" on the same lot.
|
|
16
|
+
*/
|
|
17
|
+
export declare function closeItem(item: AuctionItemRow, now?: Date): Promise<ItemOutcome | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Close every lot whose time is up, honouring anti-snipe extensions.
|
|
20
|
+
*
|
|
21
|
+
* This is what the per-minute job calls. It reads each lot's own close time
|
|
22
|
+
* (which a late bid may have pushed out seconds ago) rather than the auction's,
|
|
23
|
+
* so an extension always wins over the schedule.
|
|
24
|
+
*/
|
|
25
|
+
export declare function closeDueItems(auctionId: number, now?: Date): Promise<ItemOutcome[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Close the auction itself: resolve every remaining lot, then mark it closed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function closeAuction(auctionId: number, now?: Date): Promise<ItemOutcome[]>;
|
|
30
|
+
/**
|
|
31
|
+
* The settlement sheet: what every lot did, what the night raised, and how it
|
|
32
|
+
* measured against the goal and against fair market value.
|
|
33
|
+
*
|
|
34
|
+
* Read-only by default. Pass `{ markSettled: true }` once the school has
|
|
35
|
+
* actually invoiced, which is a decision a person makes, not a job.
|
|
36
|
+
*/
|
|
37
|
+
export declare function settleAuction(auctionId: number, opts?: { markSettled?: boolean, now?: Date }): Promise<AuctionSettlement | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Lots that close within `withinMinutes`, for the "closing soon" notice and for
|
|
40
|
+
* the organizer's watchlist of items still short of their fair market value.
|
|
41
|
+
*/
|
|
42
|
+
export declare function closingSoon(auctionId: number, withinMinutes: number, now?: Date): Promise<AuctionItemRow[]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AuctionItemRow, ItemOutcome, PlacedBid } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Told to the bidder who just lost the lead.
|
|
4
|
+
*
|
|
5
|
+
* It names the lot and the number to beat, because the entire purpose of the
|
|
6
|
+
* message is to let someone re-bid from their phone in one tap. A notice that
|
|
7
|
+
* only says "you have been outbid" makes them go find the lot themselves, and
|
|
8
|
+
* most of them do not.
|
|
9
|
+
*/
|
|
10
|
+
export declare function outbidNotification(result: PlacedBid, currency?: string): AuctionNotification | null;
|
|
11
|
+
/** Told to the winner of a lot once it closes. */
|
|
12
|
+
export declare function winnerNotification(outcome: ItemOutcome, currency?: string): AuctionNotification | null;
|
|
13
|
+
/**
|
|
14
|
+
* Told to everyone still leading a lot that is about to close, and to bidders
|
|
15
|
+
* watching one. Sent by the app's scheduler rather than inline, because "soon"
|
|
16
|
+
* is a decision about the school's evening, not about this bid.
|
|
17
|
+
*/
|
|
18
|
+
export declare function closingSoonNotification(item: AuctionItemRow, to: { name: string, email: string }, currentBid: number, currency?: string): AuctionNotification;
|
|
19
|
+
/**
|
|
20
|
+
* Notification payloads for the three things a bidder must be told.
|
|
21
|
+
*
|
|
22
|
+
* These are builders, not senders. The auction package has no opinion about
|
|
23
|
+
* whether a school reaches parents by email, SMS or a row in their dashboard
|
|
24
|
+
* inbox - it knows what happened and phrases it; `@stacksjs/notifications`
|
|
25
|
+
* decides where it goes. That split is also what lets an app schedule a notice
|
|
26
|
+
* for later (the closing-soon nudge) rather than sending it inline.
|
|
27
|
+
*/
|
|
28
|
+
export declare interface AuctionNotification {
|
|
29
|
+
to: { name: string, email: string }
|
|
30
|
+
subject: string
|
|
31
|
+
body: string
|
|
32
|
+
type: 'outbid' | 'winner' | 'closing_soon'
|
|
33
|
+
data: Record<string, unknown>
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PledgeRow, PledgeStatus } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Record a fund-a-need pledge.
|
|
4
|
+
*
|
|
5
|
+
* Fund-a-need is the part of a benefit auction that is not an auction at all:
|
|
6
|
+
* the room is asked to give at fixed levels and nobody competes for anything.
|
|
7
|
+
* It shares the auction only for the tally board, which is why it lives beside
|
|
8
|
+
* bidding but never passes through the bidding engine.
|
|
9
|
+
*/
|
|
10
|
+
export declare function makePledge(request: PledgeRequest): Promise<PledgeRow>;
|
|
11
|
+
/** Confirmed pledge money for an auction, in cents. */
|
|
12
|
+
export declare function pledgeTotal(auctionId: number): Promise<number>;
|
|
13
|
+
export declare function fetchPledges(auctionId: number): Promise<PledgeRow[]>;
|
|
14
|
+
/**
|
|
15
|
+
* The tally board: how much each level has raised and how many gave at it.
|
|
16
|
+
* Levels are reported in descending gift size, which is the order they are read
|
|
17
|
+
* out in the room.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pledgeLevels(auctionId: number): Promise<{ level: string, amount: number, count: number }[]>;
|
|
20
|
+
export declare interface PledgeRequest {
|
|
21
|
+
auctionId: number
|
|
22
|
+
donorName: string
|
|
23
|
+
donorEmail: string
|
|
24
|
+
amount: number
|
|
25
|
+
level?: string | null
|
|
26
|
+
status?: PledgeStatus
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PlacedBid, PledgeRow } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The channel an auction's live updates are published on. One channel per
|
|
4
|
+
* auction rather than per lot: a gala's bidding page shows the whole catalogue
|
|
5
|
+
* at once, and a phone in a gym should hold one socket, not forty.
|
|
6
|
+
*/
|
|
7
|
+
export declare function auctionChannel(auctionId: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Publish a bid to everyone watching the auction.
|
|
10
|
+
*
|
|
11
|
+
* The payload carries the new price and the next minimum rather than a
|
|
12
|
+
* "refresh" signal, so a phone that is already on the lot updates in place. It
|
|
13
|
+
* deliberately does NOT carry proxy ceilings: the hidden maximum is the one
|
|
14
|
+
* number that must never leave the server, or the whole mechanism collapses.
|
|
15
|
+
*/
|
|
16
|
+
export declare function broadcastBid(result: PlacedBid): Promise<void>;
|
|
17
|
+
/** Publish a fund-a-need pledge, for the running total on the tally board. */
|
|
18
|
+
export declare function broadcastPledge(pledge: PledgeRow, runningTotal: number): Promise<void>;
|
|
19
|
+
/** Publish a lot closing, so open bid sheets stop accepting input. */
|
|
20
|
+
export declare function broadcastItemClosed(auctionId: number, itemId: number, outcome: { status: string, amount: number, winnerName?: string }): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AuctionRow, AuctionRules } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The rules in force for one auction: the app's `config/auction.ts` defaults,
|
|
4
|
+
* with the auction row's own columns winning where they are set.
|
|
5
|
+
*
|
|
6
|
+
* Per-auction overrides matter because one school runs both a two-week online
|
|
7
|
+
* catalogue (long extensions, forgiving windows) and a ninety-minute in-room
|
|
8
|
+
* gala (two-minute extensions, or none at all) in the same season, and neither
|
|
9
|
+
* should have to be the global default.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rulesFor(auction?: Pick<AuctionRow, 'anti_snipe_minutes' | 'extend_on_bid_window_minutes' | 'max_extensions'> | null): AuctionRules;
|
|
12
|
+
/**
|
|
13
|
+
* The auction's currency, falling back to the storefront's. An auction
|
|
14
|
+
* inherits the currency the rest of commerce already runs in unless it says
|
|
15
|
+
* otherwise - a school does not raise money in a different one than it bills in.
|
|
16
|
+
*/
|
|
17
|
+
export declare function currencyFor(auction?: Pick<AuctionRow, 'currency'> | null): string;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export declare interface AuctionRow {
|
|
2
|
+
id: number
|
|
3
|
+
uuid?: string
|
|
4
|
+
event_id: number
|
|
5
|
+
title: string
|
|
6
|
+
description?: string | null
|
|
7
|
+
status: AuctionStatus
|
|
8
|
+
currency: string
|
|
9
|
+
goal_amount?: number | null
|
|
10
|
+
opens_at: string | Date
|
|
11
|
+
closes_at: string | Date
|
|
12
|
+
anti_snipe_minutes: number
|
|
13
|
+
extend_on_bid_window_minutes: number
|
|
14
|
+
max_extensions: number
|
|
15
|
+
}
|
|
16
|
+
export declare interface AuctionItemRow {
|
|
17
|
+
id: number
|
|
18
|
+
uuid?: string
|
|
19
|
+
auction_id: number
|
|
20
|
+
lot_number: number
|
|
21
|
+
title: string
|
|
22
|
+
description?: string | null
|
|
23
|
+
image_url?: string | null
|
|
24
|
+
category?: string | null
|
|
25
|
+
donor_name?: string | null
|
|
26
|
+
fair_market_value?: number | null
|
|
27
|
+
starting_bid: number
|
|
28
|
+
min_increment?: number | null
|
|
29
|
+
buy_now_price?: number | null
|
|
30
|
+
reserve_price?: number | null
|
|
31
|
+
status: AuctionItemStatus
|
|
32
|
+
closes_at?: string | Date | null
|
|
33
|
+
extension_count: number
|
|
34
|
+
}
|
|
35
|
+
export declare interface BidRow {
|
|
36
|
+
id: number
|
|
37
|
+
uuid?: string
|
|
38
|
+
auction_item_id: number
|
|
39
|
+
auction_id: number
|
|
40
|
+
bidder_name: string
|
|
41
|
+
bidder_email: string
|
|
42
|
+
amount: number
|
|
43
|
+
max_amount?: number | null
|
|
44
|
+
status: BidStatus
|
|
45
|
+
placed_at: string | Date
|
|
46
|
+
}
|
|
47
|
+
export declare interface PledgeRow {
|
|
48
|
+
id: number
|
|
49
|
+
uuid?: string
|
|
50
|
+
auction_id: number
|
|
51
|
+
donor_name: string
|
|
52
|
+
donor_email: string
|
|
53
|
+
amount: number
|
|
54
|
+
level?: string | null
|
|
55
|
+
status: PledgeStatus
|
|
56
|
+
created_at?: string | Date
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One rung of the increment ladder: below `upTo` cents, bids step by `step`
|
|
60
|
+
* cents. The last rung carries `upTo: null` and applies to everything above.
|
|
61
|
+
*/
|
|
62
|
+
export declare interface IncrementTier {
|
|
63
|
+
upTo: number | null
|
|
64
|
+
step: number
|
|
65
|
+
}
|
|
66
|
+
export declare interface AuctionRules {
|
|
67
|
+
increments: IncrementTier[]
|
|
68
|
+
antiSnipeMinutes: number
|
|
69
|
+
extendOnBidWindowMinutes: number
|
|
70
|
+
maxExtensions: number
|
|
71
|
+
}
|
|
72
|
+
/** What a caller passes to `placeBid`. */
|
|
73
|
+
export declare interface BidRequest {
|
|
74
|
+
itemId: number
|
|
75
|
+
bidderName: string
|
|
76
|
+
bidderEmail: string
|
|
77
|
+
amount: number
|
|
78
|
+
maxAmount?: number | null
|
|
79
|
+
now?: Date
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The result of resolving one bid against the current leader. This is what the
|
|
83
|
+
* pure engine returns; the persistence layer turns it into row writes and the
|
|
84
|
+
* caller turns it into notifications.
|
|
85
|
+
*/
|
|
86
|
+
export declare interface BidResolution {
|
|
87
|
+
accepted: boolean
|
|
88
|
+
reason?: BidRejectionReason
|
|
89
|
+
message?: string
|
|
90
|
+
leader?: {
|
|
91
|
+
bidderEmail: string
|
|
92
|
+
bidderName: string
|
|
93
|
+
amount: number
|
|
94
|
+
maxAmount: number | null
|
|
95
|
+
isChallenger: boolean
|
|
96
|
+
}
|
|
97
|
+
challengerAmount?: number
|
|
98
|
+
outbid?: {
|
|
99
|
+
bidderEmail: string
|
|
100
|
+
bidderName: string
|
|
101
|
+
amount: number
|
|
102
|
+
}
|
|
103
|
+
buyNow?: boolean
|
|
104
|
+
nextMinimumBid: number
|
|
105
|
+
}
|
|
106
|
+
export declare interface PlacedBid extends BidResolution {
|
|
107
|
+
bid?: BidRow
|
|
108
|
+
item?: AuctionItemRow
|
|
109
|
+
extendedTo?: Date
|
|
110
|
+
}
|
|
111
|
+
export declare interface ItemOutcome {
|
|
112
|
+
itemId: number
|
|
113
|
+
lotNumber: number
|
|
114
|
+
title: string
|
|
115
|
+
status: AuctionItemStatus
|
|
116
|
+
winningBidId?: number
|
|
117
|
+
winnerName?: string
|
|
118
|
+
winnerEmail?: string
|
|
119
|
+
amount: number
|
|
120
|
+
passedReason?: 'no_bids' | 'reserve_not_met'
|
|
121
|
+
}
|
|
122
|
+
export declare interface AuctionSettlement {
|
|
123
|
+
auctionId: number
|
|
124
|
+
currency: string
|
|
125
|
+
totalRaised: number
|
|
126
|
+
bidRevenue: number
|
|
127
|
+
pledgeRevenue: number
|
|
128
|
+
goalAmount: number | null
|
|
129
|
+
itemsSold: number
|
|
130
|
+
itemsPassed: number
|
|
131
|
+
sellThrough: number
|
|
132
|
+
valueDelta: number
|
|
133
|
+
outcomes: ItemOutcome[]
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Auction types.
|
|
137
|
+
*
|
|
138
|
+
* Money is integer minor units (cents) everywhere - amounts, increments,
|
|
139
|
+
* ceilings, goals and totals alike. A silent auction adds thousands of bids
|
|
140
|
+
* together and then reports the number to a board of trustees; floats lose that
|
|
141
|
+
* argument. Formatting happens at the edge, never in this package.
|
|
142
|
+
*
|
|
143
|
+
* The row shapes below are declared here rather than imported from
|
|
144
|
+
* `@stacksjs/orm`'s generated model types on purpose. This package has to build
|
|
145
|
+
* and run against an app whose ORM types were generated before the auction
|
|
146
|
+
* models existed - which is every app that installs the published framework
|
|
147
|
+
* before the next release. The models under
|
|
148
|
+
* `storage/framework/defaults/app/Models/commerce/` are the source of the
|
|
149
|
+
* columns; these interfaces are the contract the engine reads them through.
|
|
150
|
+
*/
|
|
151
|
+
/** Where an auction is in its life. */
|
|
152
|
+
export type AuctionStatus = 'draft' | 'preview' | 'open' | 'closed' | 'settled';
|
|
153
|
+
/** Where a single lot is in its life. */
|
|
154
|
+
export type AuctionItemStatus = 'open' | 'closed' | 'sold' | 'passed';
|
|
155
|
+
/**
|
|
156
|
+
* Bid state. `leading` is the one bid per item that currently wins; every
|
|
157
|
+
* other live bid is `outbid`. Both resolve to `won` / `lost` at close.
|
|
158
|
+
* `invalid` is a bid an organizer retracted (a mis-keyed amount, a guest who
|
|
159
|
+
* bid on the wrong lot), kept for the audit trail rather than deleted.
|
|
160
|
+
*/
|
|
161
|
+
export type BidStatus = 'leading' | 'outbid' | 'won' | 'lost' | 'invalid';
|
|
162
|
+
export type PledgeStatus = 'pending' | 'confirmed' | 'cancelled';
|
|
163
|
+
export type BidRejectionReason = | 'auction_not_open'
|
|
164
|
+
| 'item_not_open'
|
|
165
|
+
| 'below_minimum'
|
|
166
|
+
| 'max_below_amount'
|
|
167
|
+
| 'already_leading'
|
|
168
|
+
| 'item_not_found';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as auctions from './auctions/index';
|
|
1
2
|
import * as coupons from './coupons/index';
|
|
2
3
|
import * as customers from './customers/index';
|
|
3
4
|
import * as devices from './devices/index';
|
|
@@ -13,6 +14,7 @@ import * as tax from './tax/index';
|
|
|
13
14
|
import * as waitlists from './waitlists/index';
|
|
14
15
|
export declare const commerce: CommerceNamespace;
|
|
15
16
|
export declare interface CommerceNamespace {
|
|
17
|
+
auctions: AuctionsModule
|
|
16
18
|
coupons: CouponsModule
|
|
17
19
|
customers: CustomersModule
|
|
18
20
|
errors: ErrorsModule
|
|
@@ -27,6 +29,7 @@ export declare interface CommerceNamespace {
|
|
|
27
29
|
devices: DevicesModule
|
|
28
30
|
receipts: ReceiptsModule
|
|
29
31
|
}
|
|
32
|
+
declare type AuctionsModule = typeof auctions;
|
|
30
33
|
declare type CouponsModule = typeof coupons;
|
|
31
34
|
declare type CustomersModule = typeof customers;
|
|
32
35
|
declare type ErrorsModule = typeof errors;
|
|
@@ -41,6 +44,7 @@ declare type WaitlistsModule = typeof waitlists;
|
|
|
41
44
|
declare type DevicesModule = typeof devices;
|
|
42
45
|
declare type ReceiptsModule = typeof receipts;
|
|
43
46
|
export {
|
|
47
|
+
auctions,
|
|
44
48
|
coupons,
|
|
45
49
|
customers,
|
|
46
50
|
devices,
|