@agent-cards/commerce-sdk 0.2.0 → 0.5.0

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.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Every Agentcard event a shopper should hear about, read as one message for that shopper's chat.
3
+ * `purchase.message` carries the text Agentcard wrote. `order.confirmed` and `order.updated` carry
4
+ * facts only, so the package writes their text. Every other event type is not a shopper message.
5
+ */
6
+ import { type AgentcardEvent } from './verify.js';
7
+ import { type PurchaseFacts, type PurchaseMessageKind } from './message.js';
8
+ /**
9
+ * The event types that carry a message for a shopper. Subscribe your endpoint to each by name:
10
+ * a catch-all `*` alone does not turn `purchase.message` on.
11
+ */
12
+ export declare const SHOPPER_EVENT_TYPES: readonly ["purchase.message", "order.confirmed", "order.updated"];
13
+ /** One of the event types that carry a message for a shopper. */
14
+ export type ShopperEventType = (typeof SHOPPER_EVENT_TYPES)[number];
15
+ /**
16
+ * The delivery stage of an order, as `order.updated` names it. A stage Agentcard adds later reads
17
+ * as its own name.
18
+ */
19
+ export type OrderStatus = 'confirmed' | 'courier_assigned' | 'courier_at_store' | 'ready_for_pickup' | 'picked_up' | 'arriving' | 'delivered' | 'canceled';
20
+ /**
21
+ * The moment a shopper message is about. The `purchase.message` kinds, then:
22
+ * - `order_confirmed`: the retailer accepted the order on its side (`order.confirmed`).
23
+ * - `order_updated`: the order reached a new delivery stage, named in `facts.orderStatus` (`order.updated`).
24
+ */
25
+ export type ShopperMessageKind = PurchaseMessageKind | 'order_confirmed' | 'order_updated';
26
+ /** The values of a shopper message one by one. A fact that does not apply is absent. */
27
+ export interface ShopperFacts extends PurchaseFacts {
28
+ /** The retailer's own order number, as on its confirmation and account page. */
29
+ retailerOrderId?: string;
30
+ /** The retailer's quoted delivery window, in its own words. */
31
+ deliveryWindow?: string;
32
+ /** The order's delivery stage. */
33
+ orderStatus?: OrderStatus | (string & {});
34
+ /** The stage before this one. */
35
+ previousOrderStatus?: OrderStatus | (string & {});
36
+ /** The store's own line for the stage, such as "Your Dasher is nearby". */
37
+ statusTitle?: string;
38
+ /** The earliest and the latest arrival the store estimates, as ISO 8601 timestamps. */
39
+ arrivalEarliest?: string;
40
+ arrivalLatest?: string;
41
+ /** When the order was delivered, as an ISO 8601 timestamp. */
42
+ deliveredAt?: string;
43
+ /** Why the store canceled the order, in its own word. */
44
+ canceledReason?: string;
45
+ }
46
+ /** A message for one shopper's chat, read from any event that carries one. */
47
+ export interface ShopperMessage {
48
+ /** The event id. Agentcard delivers at least once: handle each id once. */
49
+ eventId: string;
50
+ /** The event type the message arrived as. */
51
+ type: ShopperEventType;
52
+ /**
53
+ * The moment the message is about: one of the kinds above, or a kind Agentcard adds later, read
54
+ * as its own name. Give a `switch` over it a default branch that sends `text`.
55
+ */
56
+ kind: ShopperMessageKind | (string & {});
57
+ /** The shopper's Agentcard user id. */
58
+ userId: string;
59
+ /** Your own id for the shopper, when you gave Agentcard one: the chat to post `text` into. */
60
+ externalUserId: string | null;
61
+ /** The buy conversation the event belongs to, when Agentcard names one. */
62
+ conversationId: string | null;
63
+ /** Ready to send to the shopper as is. */
64
+ text: string;
65
+ /** For your agent: the facts of what happened, marked as written by the system and not typed by the shopper. */
66
+ note: string;
67
+ facts: ShopperFacts;
68
+ /** The OAuth client id of the app the shopper connected through, when the event names it. */
69
+ clientId: string | null;
70
+ /** False for a sandbox event. */
71
+ livemode: boolean;
72
+ /** Unix seconds. */
73
+ created: number;
74
+ }
75
+ /**
76
+ * The message an event carries for a shopper, or null when it carries none: an event type that is
77
+ * not a shopper event, an order event that names no shopper, or a delivery stage the package does
78
+ * not know and the store gave no line for. Pass the event `verifyAgentcardWebhook` returned.
79
+ * Throws a TypeError when a `purchase.message` lacks its kind, user_id or text.
80
+ */
81
+ export declare function readShopperMessage(event: AgentcardEvent): ShopperMessage | null;
82
+ /**
83
+ * Whether the message stopped holding before `now` (unix milliseconds, default the system clock):
84
+ * the link or the window its text names has closed. Drop a stale message instead of sending it.
85
+ */
86
+ export declare function isStale(message: Pick<ShopperMessage, 'facts'>, now?: number): boolean;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Every Agentcard event a shopper should hear about, read as one message for that shopper's chat.
3
+ * `purchase.message` carries the text Agentcard wrote. `order.confirmed` and `order.updated` carry
4
+ * facts only, so the package writes their text. Every other event type is not a shopper message.
5
+ */
6
+ import { isRecord } from './verify.js';
7
+ import { readPurchaseMessage } from './message.js';
8
+ /**
9
+ * The event types that carry a message for a shopper. Subscribe your endpoint to each by name:
10
+ * a catch-all `*` alone does not turn `purchase.message` on.
11
+ */
12
+ export const SHOPPER_EVENT_TYPES = ['purchase.message', 'order.confirmed', 'order.updated'];
13
+ const SYSTEM_LINE = 'This note was generated by the system, not typed by the user.';
14
+ function text(value, max = 200) {
15
+ if (typeof value !== 'string')
16
+ return undefined;
17
+ // One line, so a value never breaks the sentence it sits in.
18
+ const line = value.replace(/\s+/g, ' ').trim();
19
+ return line === '' ? undefined : line.slice(0, max);
20
+ }
21
+ /** An id exactly as Agentcard sent it, so a lookup by it finds the same record the other events name. */
22
+ function id(value) {
23
+ return typeof value === 'string' && value !== '' ? value : undefined;
24
+ }
25
+ function count(value) {
26
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
27
+ }
28
+ /** Facts without the values that do not apply. */
29
+ function present(facts) {
30
+ return Object.fromEntries(Object.entries(facts).filter(([, value]) => value !== undefined));
31
+ }
32
+ /**
33
+ * The store as the shopper knows it, or null when the event names a roster of stores rather than
34
+ * one: a retail order names every retailer it can reach.
35
+ */
36
+ function storeOf(data) {
37
+ if (data.merchant === 'retail')
38
+ return null;
39
+ return text(data.merchant_name, 64) ?? null;
40
+ }
41
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
42
+ /** "Your DoorDash order is almost there." for each stage, or null for a stage the package does not know. */
43
+ function stageText(status, store, reason, title) {
44
+ const order = store ? `your ${store} order` : 'your order';
45
+ switch (status) {
46
+ case 'confirmed':
47
+ return store ? `${store} accepted your order.` : 'The store accepted your order.';
48
+ case 'courier_assigned':
49
+ return `A driver is assigned to ${order}.`;
50
+ case 'courier_at_store':
51
+ return `The driver is at the store for ${order}.`;
52
+ case 'ready_for_pickup':
53
+ return `${capitalize(order)} is ready for pickup.`;
54
+ case 'picked_up':
55
+ return `${capitalize(order)} is picked up and on its way.`;
56
+ case 'arriving':
57
+ return `${capitalize(order)} is almost there.`;
58
+ case 'delivered':
59
+ return `${capitalize(order)} was delivered.`;
60
+ case 'canceled':
61
+ return `${capitalize(order)} was canceled${reason ? `: ${reason}` : ''}.`;
62
+ default:
63
+ return title ? `${store ? `${store}: ` : ''}${title}` : null;
64
+ }
65
+ }
66
+ function orderBase(event, type) {
67
+ const data = event.data;
68
+ const userId = id(data.user_id);
69
+ if (!userId)
70
+ return null;
71
+ const connection = isRecord(data.connection) ? data.connection : {};
72
+ return {
73
+ data,
74
+ store: storeOf(data),
75
+ base: {
76
+ eventId: event.id,
77
+ type,
78
+ userId,
79
+ externalUserId: id(data.external_user_id) ?? null,
80
+ conversationId: id(data.conversation_id) ?? null,
81
+ clientId: id(connection.client_id) ?? null,
82
+ livemode: event.livemode,
83
+ created: event.created,
84
+ },
85
+ };
86
+ }
87
+ function readOrderConfirmed(event) {
88
+ const read = orderBase(event, 'order.confirmed');
89
+ if (!read)
90
+ return null;
91
+ const { data, store, base } = read;
92
+ const retailerOrderId = text(data.retailer_order_id, 80);
93
+ const deliveryWindow = text(data.delivery_window, 80);
94
+ const facts = present({
95
+ merchantName: store ?? undefined,
96
+ totalCents: count(data.final_total_cents) ?? count(data.charged_cents),
97
+ currency: text(data.currency, 8),
98
+ orderId: id(data.order_id),
99
+ retailerOrderId,
100
+ deliveryWindow,
101
+ });
102
+ const who = store ? `${store} confirmed your order` : 'The store confirmed your order';
103
+ return {
104
+ ...base,
105
+ kind: 'order_confirmed',
106
+ text: `${who}${retailerOrderId ? `, order number ${retailerOrderId}` : ''}.${deliveryWindow ? ` Expected delivery: ${deliveryWindow}.` : ''}`,
107
+ note: `[event: the store accepted this user's order${store ? ` at ${store}` : ''} on its side, outside this conversation.` +
108
+ `${retailerOrderId ? ` The store's order number is ${retailerOrderId}.` : ''}` +
109
+ `${deliveryWindow ? ` The store quotes delivery ${deliveryWindow}.` : ''} ${SYSTEM_LINE}]`,
110
+ facts,
111
+ };
112
+ }
113
+ function readOrderUpdated(event) {
114
+ const read = orderBase(event, 'order.updated');
115
+ if (!read)
116
+ return null;
117
+ const { data, store, base } = read;
118
+ const status = text(data.status, 40);
119
+ if (!status)
120
+ return null;
121
+ const statusTitle = text(data.status_title, 120);
122
+ const canceledReason = text(data.canceled_reason, 120);
123
+ const message = stageText(status, store, canceledReason, statusTitle);
124
+ if (!message)
125
+ return null;
126
+ const arrival = isRecord(data.estimated_arrival) ? data.estimated_arrival : {};
127
+ const previous = text(data.previous_status, 40);
128
+ return {
129
+ ...base,
130
+ kind: 'order_updated',
131
+ text: message,
132
+ note: `[event: this user's order${store ? ` at ${store}` : ''} moved to the stage ${status}` +
133
+ `${previous ? ` from ${previous}` : ''}, outside this conversation.` +
134
+ `${statusTitle ? ` The store says: ${statusTitle}.` : ''}${canceledReason ? ` The store's reason: ${canceledReason}.` : ''} ${SYSTEM_LINE}]`,
135
+ facts: present({
136
+ merchantName: store ?? undefined,
137
+ orderId: id(data.order_id),
138
+ orderStatus: status,
139
+ previousOrderStatus: previous,
140
+ statusTitle,
141
+ arrivalEarliest: text(arrival.earliest, 40),
142
+ arrivalLatest: text(arrival.latest, 40),
143
+ deliveredAt: text(data.delivered_at, 40),
144
+ canceledReason,
145
+ }),
146
+ };
147
+ }
148
+ /**
149
+ * The message an event carries for a shopper, or null when it carries none: an event type that is
150
+ * not a shopper event, an order event that names no shopper, or a delivery stage the package does
151
+ * not know and the store gave no line for. Pass the event `verifyAgentcardWebhook` returned.
152
+ * Throws a TypeError when a `purchase.message` lacks its kind, user_id or text.
153
+ */
154
+ export function readShopperMessage(event) {
155
+ if (event.type === 'order.confirmed')
156
+ return readOrderConfirmed(event);
157
+ if (event.type === 'order.updated')
158
+ return readOrderUpdated(event);
159
+ const message = readPurchaseMessage(event);
160
+ if (!message)
161
+ return null;
162
+ return {
163
+ ...message,
164
+ type: 'purchase.message',
165
+ conversationId: id(event.data.conversation_id) ?? null,
166
+ };
167
+ }
168
+ /**
169
+ * Whether the message stopped holding before `now` (unix milliseconds, default the system clock):
170
+ * the link or the window its text names has closed. Drop a stale message instead of sending it.
171
+ */
172
+ export function isStale(message, now = Date.now()) {
173
+ const until = message.facts.validUntil ? Date.parse(message.facts.validUntil) : Number.NaN;
174
+ return Number.isFinite(until) && until <= now;
175
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agent-cards/commerce-sdk",
3
- "version": "0.2.0",
4
- "description": "Give your agent one tool that buys for your shoppers at real stores, and hear what happened to each purchase from a signed webhook.",
3
+ "version": "0.5.0",
4
+ "description": "Give your agent one tool that buys for your shoppers at real stores, and post each purchase update Agentcard sends into the shopper's chat.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",