@divythebill/schema 0.1.1 → 0.2.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/index.cjs CHANGED
@@ -1,5 +1,270 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/index.ts
3
- const SCHEMA_PACKAGE_NAME = "@divythebill/schema";
2
+ let zod = require("zod");
3
+ //#region src/constants.ts
4
+ /** Maximum quantity allowed for a single line item. */
5
+ const MAX_ITEM_QUANTITY = 999;
6
+ /** Maximum magnitude (absolute value) allowed for an item's unit price. */
7
+ const MAX_ITEM_PRICE = 1e6;
8
+ /**
9
+ * Upper bound for an item's total price. Derived from the unit-price and
10
+ * quantity caps (total = unit × quantity), so valid input can never exceed it.
11
+ */
12
+ const MAX_ITEM_TOTAL_PRICE = MAX_ITEM_PRICE * 999;
13
+ /** Fallback avatar/profile color used when a user hasn't picked one. */
14
+ const DEFAULT_AVATAR_COLOR = "#bfdbfe";
15
+ /** Collection holding receipt documents. */
16
+ const RECEIPTS_COLLECTION = "receipts";
17
+ /** Subcollection name for a receipt's line items. */
18
+ const RECEIPT_ITEMS_COLLECTION = "items";
4
19
  //#endregion
5
- exports.SCHEMA_PACKAGE_NAME = SCHEMA_PACKAGE_NAME;
20
+ //#region src/itemModifier.ts
21
+ const itemModifierSchema = zod.z.object({ modifier_description: zod.z.string().default("") });
22
+ //#endregion
23
+ //#region src/item.ts
24
+ const itemSchema = zod.z.object({
25
+ item_id: zod.z.uuid(),
26
+ item_name: zod.z.string().min(1, "Item name is required."),
27
+ item_note: zod.z.string().optional().default(""),
28
+ item_quantity: zod.z.int().positive().max(999),
29
+ item_unit_price: zod.z.number().min(-1e6).max(MAX_ITEM_PRICE),
30
+ item_total_price: zod.z.number().min(-999e6).max(MAX_ITEM_TOTAL_PRICE),
31
+ item_modifiers: zod.z.array(itemModifierSchema).default([]),
32
+ item_assigned_uids: zod.z.array(zod.z.string()).default([]),
33
+ item_order: zod.z.number().default(0)
34
+ });
35
+ /**
36
+ * READ schema for item documents. `itemSchema` answers "may this be submitted
37
+ * as finished?" (form validation); this answers "can we render what's actually
38
+ * stored?" So the name may be empty: a just-added item is written unnamed
39
+ * while it's being edited, and the read layer must still surface it.
40
+ */
41
+ const itemReadSchema = itemSchema.extend({ item_name: zod.z.string().default("") });
42
+ const guestSchema = zod.z.object({
43
+ type: zod.z.literal("guest").default("guest"),
44
+ uid: zod.z.uuid(),
45
+ first_name: zod.z.string().default(""),
46
+ last_name: zod.z.string().optional().default(""),
47
+ emoji: zod.z.string().optional().default(""),
48
+ venmo: zod.z.string().trim().refine((val) => !val || !val.includes(" "), "Venmo handles cannot contain spaces").optional().default(""),
49
+ cashapp: zod.z.string().trim().refine((val) => !val || !val.includes(" "), "Cashtags cannot contain spaces").optional().default(""),
50
+ phone_number: zod.z.string().optional().default(""),
51
+ color: zod.z.string()
52
+ }).refine((guest) => guest.first_name.trim().length > 0 || guest.emoji.length > 0, {
53
+ path: ["first_name"],
54
+ message: "First name is required."
55
+ });
56
+ const profileRefSchema = zod.z.object({
57
+ type: zod.z.literal("member").default("member"),
58
+ uid: zod.z.string()
59
+ });
60
+ const departedRefSchema = zod.z.object({
61
+ type: zod.z.literal("departed"),
62
+ uid: zod.z.string(),
63
+ first_name: zod.z.string().default(""),
64
+ last_name: zod.z.string().default(""),
65
+ color: zod.z.string().default(DEFAULT_AVATAR_COLOR)
66
+ });
67
+ //#endregion
68
+ //#region src/receipt.ts
69
+ const receiptSchema = zod.z.object({
70
+ receipt_id: zod.z.string(),
71
+ owner_id: zod.z.string(),
72
+ shared_with: zod.z.array(zod.z.discriminatedUnion("type", [
73
+ profileRefSchema,
74
+ guestSchema,
75
+ departedRefSchema
76
+ ])).default([]),
77
+ shared_with_uids: zod.z.array(zod.z.string()).default([]),
78
+ source_image: zod.z.url().optional(),
79
+ receipt_status: zod.z.enum([
80
+ "draft",
81
+ "in_progress",
82
+ "completed"
83
+ ]).default("draft"),
84
+ merchant_name: zod.z.string().min(1, "Merchant name is required.").default(""),
85
+ merchant_address: zod.z.string().default(""),
86
+ merchant_full_address: zod.z.string().default(""),
87
+ merchant_city: zod.z.string().default(""),
88
+ merchant_state: zod.z.string().default(""),
89
+ merchant_google_maps_uri: zod.z.url().optional(),
90
+ original_upload_time: zod.z.coerce.date().default(/* @__PURE__ */ new Date()),
91
+ transaction_date_time: zod.z.coerce.date().default(/* @__PURE__ */ new Date()),
92
+ server_name: zod.z.string().trim().default("Not Listed"),
93
+ guest_count: zod.z.number().int().positive().default(1).catch(1),
94
+ currency_code: zod.z.string().default("USD"),
95
+ subtotal: zod.z.coerce.number().default(0),
96
+ tax: zod.z.coerce.number().default(0),
97
+ tip: zod.z.coerce.number().default(0),
98
+ additional_tip: zod.z.coerce.number().optional().default(0),
99
+ service_fee: zod.z.coerce.number().optional().default(0),
100
+ discount: zod.z.coerce.number().optional().default(0),
101
+ grand_total: zod.z.coerce.number().default(0),
102
+ card_issuer: zod.z.string().toUpperCase().optional(),
103
+ card_id: zod.z.string().optional(),
104
+ deleted_at: zod.z.coerce.date().optional()
105
+ });
106
+ /**
107
+ * READ schema for receipt documents. `receiptSchema` answers "may this be
108
+ * submitted as finished?"; this answers "can we render what's actually
109
+ * stored?" Completion rules are relaxed because incomplete docs exist by
110
+ * design (drafts, best-effort extraction); structural rules (types, ranges,
111
+ * enums) still apply, so genuine corruption is skipped rather than rendered.
112
+ */
113
+ const receiptReadSchema = receiptSchema.extend({ merchant_name: zod.z.string().default("") });
114
+ //#endregion
115
+ //#region src/split.ts
116
+ const splitResultSchema = zod.z.object({ users: zod.z.array(zod.z.object({
117
+ uid: zod.z.string(),
118
+ items: zod.z.array(zod.z.object({
119
+ item_id: zod.z.uuid(),
120
+ item_name: zod.z.string(),
121
+ item_split_price: zod.z.number()
122
+ })),
123
+ subtotal: zod.z.number(),
124
+ tax: zod.z.number(),
125
+ tip: zod.z.number(),
126
+ additional_tip: zod.z.number(),
127
+ service_fee: zod.z.number(),
128
+ discount: zod.z.number(),
129
+ total: zod.z.number()
130
+ })) });
131
+ //#endregion
132
+ //#region src/extraction.ts
133
+ const extractedItemSchema = zod.z.object({
134
+ item_name: zod.z.string().describe("Name of the product, fee, or discount."),
135
+ item_quantity: zod.z.int().describe("Unit count; integer, default 1."),
136
+ item_unit_price: zod.z.number().describe("Price per unit, decimal; negative for discounts."),
137
+ item_total_price: zod.z.number().describe("Line total (quantity * unit_price), decimal; negative for discounts."),
138
+ item_modifiers: zod.z.array(zod.z.object({ modifier_description: zod.z.string().describe("The modifier text (e.g., 'No Onions').") })).describe("Unpriced descriptive lines belonging to this item; empty array if none.")
139
+ });
140
+ const receiptExtractionSchema = zod.z.object({
141
+ merchant_name: zod.z.string().describe("Establishment or vendor name."),
142
+ merchant_address: zod.z.string().describe("Full street address as printed."),
143
+ merchant_city: zod.z.string().optional().describe("City."),
144
+ merchant_state: zod.z.string().optional().describe("State."),
145
+ transaction_date_time: zod.z.string().describe("Purchase datetime, ISO 8601 (YYYY-MM-DDThh:mm:ss)."),
146
+ server_name: zod.z.string().describe("Server's name; default 'Not Listed'."),
147
+ guest_count: zod.z.int().describe("Number of guests; integer >= 1, default 1."),
148
+ currency_code: zod.z.string().describe("3-letter ISO currency code (e.g., USD); default USD."),
149
+ items: zod.z.array(extractedItemSchema).describe("Line items: products, flat per-item fees, and discounts (discounts have negative prices)."),
150
+ subtotal: zod.z.number().describe("Items total before tax/tip/fees, decimal; equals the sum of all item_total_price."),
151
+ tax: zod.z.number().describe("Total tax, decimal."),
152
+ tip: zod.z.number().describe("Staff gratuity, decimal."),
153
+ additional_tip: zod.z.number().optional().describe("Extra gratuity beyond the primary tip, decimal."),
154
+ service_fee: zod.z.number().optional().describe("Whole-bill non-tax, non-gratuity fees, summed into one decimal."),
155
+ discount: zod.z.number().optional().describe("Global bill-wide discount, negative decimal."),
156
+ grand_total: zod.z.number().describe("Final amount paid, decimal."),
157
+ card_issuer: zod.z.string().optional().describe("Card issuer shorthand (e.g., Visa, MC, Amex, Disc)."),
158
+ card_id: zod.z.string().optional().describe("Last 4-5 digits of the first card used.")
159
+ });
160
+ //#endregion
161
+ //#region src/sanitize.ts
162
+ function clamp(value, min, max) {
163
+ return Math.min(Math.max(value, min), max);
164
+ }
165
+ /** A string no matter what: junk becomes "", and the result is trimmed. */
166
+ const lenientTrimmedString = zod.z.string().catch("").transform((value) => value.trim());
167
+ /**
168
+ * A 2-letter uppercase state/region code, or "". Truncating a full name would
169
+ * fabricate a wrong code ("Texas" → "TE"; "Arizona" → "AR", which is
170
+ * Arkansas), so anything that isn't already a 2-letter code becomes "".
171
+ */
172
+ const stateCodeSchema = lenientTrimmedString.transform((value) => {
173
+ const state = value.toUpperCase();
174
+ return /^[A-Z]{2}$/.test(state) ? state : "";
175
+ });
176
+ /**
177
+ * Maps one extracted line item (plus the caller-supplied identity fields) to
178
+ * exactly the item document shape consumers store and read, with every value
179
+ * forced into contract bounds. Output always satisfies `ItemRead`.
180
+ *
181
+ * Usage: `sanitizedItemSchema.parse({ ...rawExtractedItem, item_id, item_order })`
182
+ */
183
+ const sanitizedItemSchema = zod.z.object({
184
+ item_id: zod.z.uuid(),
185
+ item_order: zod.z.number().catch(0),
186
+ item_name: lenientTrimmedString,
187
+ item_note: zod.z.string().catch("").default(""),
188
+ item_quantity: zod.z.number().catch(1).transform((quantity) => clamp(Math.round(quantity), 1, 999)),
189
+ item_unit_price: zod.z.number().catch(0).transform((price) => clamp(price, -1e6, MAX_ITEM_PRICE)),
190
+ item_total_price: zod.z.number().catch(0).transform((price) => clamp(price, -999e6, MAX_ITEM_TOTAL_PRICE)),
191
+ item_modifiers: zod.z.array(zod.z.object({ modifier_description: lenientTrimmedString }).catch({ modifier_description: "" })).catch([]).transform((modifiers) => modifiers.filter((modifier) => modifier.modifier_description.length > 0)),
192
+ item_assigned_uids: zod.z.array(zod.z.string()).catch([]).default([])
193
+ });
194
+ //#endregion
195
+ //#region src/gemini.ts
196
+ const GEMINI_SUPPORTED_KEYWORDS = /* @__PURE__ */ new Set([
197
+ "$id",
198
+ "$defs",
199
+ "$ref",
200
+ "$anchor",
201
+ "type",
202
+ "format",
203
+ "title",
204
+ "description",
205
+ "enum",
206
+ "items",
207
+ "prefixItems",
208
+ "minItems",
209
+ "maxItems",
210
+ "minimum",
211
+ "maximum",
212
+ "anyOf",
213
+ "oneOf",
214
+ "properties",
215
+ "additionalProperties",
216
+ "required",
217
+ "propertyOrdering"
218
+ ]);
219
+ const NAME_TO_SCHEMA_KEYWORDS = ["properties", "$defs"];
220
+ const SCHEMA_VALUE_KEYWORDS = [
221
+ "items",
222
+ "additionalProperties",
223
+ "prefixItems",
224
+ "anyOf",
225
+ "oneOf"
226
+ ];
227
+ function isObject(value) {
228
+ return typeof value === "object" && value !== null && !Array.isArray(value);
229
+ }
230
+ function sanitizeSchemaNode(node) {
231
+ const result = {};
232
+ for (const [keyword, value] of Object.entries(node)) {
233
+ if (!GEMINI_SUPPORTED_KEYWORDS.has(keyword)) continue;
234
+ if (NAME_TO_SCHEMA_KEYWORDS.includes(keyword) && isObject(value)) result[keyword] = Object.fromEntries(Object.entries(value).map(([fieldName, subSchema]) => [fieldName, isObject(subSchema) ? sanitizeSchemaNode(subSchema) : subSchema]));
235
+ else if (SCHEMA_VALUE_KEYWORDS.includes(keyword)) if (Array.isArray(value)) result[keyword] = value.map((subSchema) => isObject(subSchema) ? sanitizeSchemaNode(subSchema) : subSchema);
236
+ else result[keyword] = isObject(value) ? sanitizeSchemaNode(value) : value;
237
+ else result[keyword] = value;
238
+ }
239
+ if (isObject(result.properties) && !result.propertyOrdering) result.propertyOrdering = Object.keys(result.properties);
240
+ return result;
241
+ }
242
+ /**
243
+ * The Gemini `responseJsonSchema` value for a zod schema. Feed the result to
244
+ * `generateContent`'s config alongside `responseMimeType: "application/json"`.
245
+ */
246
+ function toGeminiSchema(schema) {
247
+ return sanitizeSchemaNode(zod.z.toJSONSchema(schema));
248
+ }
249
+ //#endregion
250
+ exports.DEFAULT_AVATAR_COLOR = DEFAULT_AVATAR_COLOR;
251
+ exports.GEMINI_SUPPORTED_KEYWORDS = GEMINI_SUPPORTED_KEYWORDS;
252
+ exports.MAX_ITEM_PRICE = MAX_ITEM_PRICE;
253
+ exports.MAX_ITEM_QUANTITY = MAX_ITEM_QUANTITY;
254
+ exports.MAX_ITEM_TOTAL_PRICE = MAX_ITEM_TOTAL_PRICE;
255
+ exports.RECEIPTS_COLLECTION = RECEIPTS_COLLECTION;
256
+ exports.RECEIPT_ITEMS_COLLECTION = RECEIPT_ITEMS_COLLECTION;
257
+ exports.departedRefSchema = departedRefSchema;
258
+ exports.extractedItemSchema = extractedItemSchema;
259
+ exports.guestSchema = guestSchema;
260
+ exports.itemModifierSchema = itemModifierSchema;
261
+ exports.itemReadSchema = itemReadSchema;
262
+ exports.itemSchema = itemSchema;
263
+ exports.profileRefSchema = profileRefSchema;
264
+ exports.receiptExtractionSchema = receiptExtractionSchema;
265
+ exports.receiptReadSchema = receiptReadSchema;
266
+ exports.receiptSchema = receiptSchema;
267
+ exports.sanitizedItemSchema = sanitizedItemSchema;
268
+ exports.splitResultSchema = splitResultSchema;
269
+ exports.stateCodeSchema = stateCodeSchema;
270
+ exports.toGeminiSchema = toGeminiSchema;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,314 @@
1
- //#region src/index.d.ts
2
- declare const SCHEMA_PACKAGE_NAME = "@divythebill/schema";
1
+ import { z } from "zod";
2
+ //#region src/constants.d.ts
3
+ /** Maximum quantity allowed for a single line item. */
4
+ declare const MAX_ITEM_QUANTITY = 999;
5
+ /** Maximum magnitude (absolute value) allowed for an item's unit price. */
6
+ declare const MAX_ITEM_PRICE = 1000000;
7
+ /**
8
+ * Upper bound for an item's total price. Derived from the unit-price and
9
+ * quantity caps (total = unit × quantity), so valid input can never exceed it.
10
+ */
11
+ declare const MAX_ITEM_TOTAL_PRICE: number;
12
+ /** Fallback avatar/profile color used when a user hasn't picked one. */
13
+ declare const DEFAULT_AVATAR_COLOR = "#bfdbfe";
14
+ /** Collection holding receipt documents. */
15
+ declare const RECEIPTS_COLLECTION = "receipts";
16
+ /** Subcollection name for a receipt's line items. */
17
+ declare const RECEIPT_ITEMS_COLLECTION = "items";
3
18
  //#endregion
4
- export { SCHEMA_PACKAGE_NAME };
19
+ //#region src/itemModifier.d.ts
20
+ declare const itemModifierSchema: z.ZodObject<{
21
+ modifier_description: z.ZodDefault<z.ZodString>;
22
+ }, z.core.$strip>;
23
+ type ItemModifier = z.infer<typeof itemModifierSchema>;
24
+ //#endregion
25
+ //#region src/item.d.ts
26
+ declare const itemSchema: z.ZodObject<{
27
+ item_id: z.ZodUUID;
28
+ item_name: z.ZodString;
29
+ item_note: z.ZodDefault<z.ZodOptional<z.ZodString>>;
30
+ item_quantity: z.ZodInt;
31
+ item_unit_price: z.ZodNumber;
32
+ item_total_price: z.ZodNumber;
33
+ item_modifiers: z.ZodDefault<z.ZodArray<z.ZodObject<{
34
+ modifier_description: z.ZodDefault<z.ZodString>;
35
+ }, z.core.$strip>>>;
36
+ item_assigned_uids: z.ZodDefault<z.ZodArray<z.ZodString>>;
37
+ item_order: z.ZodDefault<z.ZodNumber>;
38
+ }, z.core.$strip>;
39
+ type Item = z.infer<typeof itemSchema>;
40
+ /**
41
+ * READ schema for item documents. `itemSchema` answers "may this be submitted
42
+ * as finished?" (form validation); this answers "can we render what's actually
43
+ * stored?" So the name may be empty: a just-added item is written unnamed
44
+ * while it's being edited, and the read layer must still surface it.
45
+ */
46
+ declare const itemReadSchema: z.ZodObject<{
47
+ item_id: z.ZodUUID;
48
+ item_note: z.ZodDefault<z.ZodOptional<z.ZodString>>;
49
+ item_quantity: z.ZodInt;
50
+ item_unit_price: z.ZodNumber;
51
+ item_total_price: z.ZodNumber;
52
+ item_modifiers: z.ZodDefault<z.ZodArray<z.ZodObject<{
53
+ modifier_description: z.ZodDefault<z.ZodString>;
54
+ }, z.core.$strip>>>;
55
+ item_assigned_uids: z.ZodDefault<z.ZodArray<z.ZodString>>;
56
+ item_order: z.ZodDefault<z.ZodNumber>;
57
+ item_name: z.ZodDefault<z.ZodString>;
58
+ }, z.core.$strip>;
59
+ /**
60
+ * The lenient shape of an item as stored / displayed — the display and
61
+ * in-progress-draft type. `Item` (from `itemSchema`) is the strict, validated
62
+ * write shape; a draft is narrowed to `Item` via `itemSchema` only at commit.
63
+ * Everything that reads or edits uses `ItemRead`; everything that *writes*
64
+ * takes `Item`.
65
+ */
66
+ type ItemRead = z.infer<typeof itemReadSchema>;
67
+ //#endregion
68
+ //#region src/participant.d.ts
69
+ declare const guestSchema: z.ZodObject<{
70
+ type: z.ZodDefault<z.ZodLiteral<"guest">>;
71
+ uid: z.ZodUUID;
72
+ first_name: z.ZodDefault<z.ZodString>;
73
+ last_name: z.ZodDefault<z.ZodOptional<z.ZodString>>;
74
+ emoji: z.ZodDefault<z.ZodOptional<z.ZodString>>;
75
+ venmo: z.ZodDefault<z.ZodOptional<z.ZodString>>;
76
+ cashapp: z.ZodDefault<z.ZodOptional<z.ZodString>>;
77
+ phone_number: z.ZodDefault<z.ZodOptional<z.ZodString>>;
78
+ color: z.ZodString;
79
+ }, z.core.$strip>;
80
+ type Guest = z.infer<typeof guestSchema>;
81
+ type GuestInput = z.input<typeof guestSchema>;
82
+ declare const profileRefSchema: z.ZodObject<{
83
+ type: z.ZodDefault<z.ZodLiteral<"member">>;
84
+ uid: z.ZodString;
85
+ }, z.core.$strip>;
86
+ type ProfileRef = z.infer<typeof profileRefSchema>;
87
+ declare const departedRefSchema: z.ZodObject<{
88
+ type: z.ZodLiteral<"departed">;
89
+ uid: z.ZodString;
90
+ first_name: z.ZodDefault<z.ZodString>;
91
+ last_name: z.ZodDefault<z.ZodString>;
92
+ color: z.ZodDefault<z.ZodString>;
93
+ }, z.core.$strip>;
94
+ type DepartedRef = z.infer<typeof departedRefSchema>;
95
+ //#endregion
96
+ //#region src/receipt.d.ts
97
+ declare const receiptSchema: z.ZodObject<{
98
+ receipt_id: z.ZodString;
99
+ owner_id: z.ZodString;
100
+ shared_with: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
101
+ type: z.ZodDefault<z.ZodLiteral<"member">>;
102
+ uid: z.ZodString;
103
+ }, z.core.$strip>, z.ZodObject<{
104
+ type: z.ZodDefault<z.ZodLiteral<"guest">>;
105
+ uid: z.ZodUUID;
106
+ first_name: z.ZodDefault<z.ZodString>;
107
+ last_name: z.ZodDefault<z.ZodOptional<z.ZodString>>;
108
+ emoji: z.ZodDefault<z.ZodOptional<z.ZodString>>;
109
+ venmo: z.ZodDefault<z.ZodOptional<z.ZodString>>;
110
+ cashapp: z.ZodDefault<z.ZodOptional<z.ZodString>>;
111
+ phone_number: z.ZodDefault<z.ZodOptional<z.ZodString>>;
112
+ color: z.ZodString;
113
+ }, z.core.$strip>, z.ZodObject<{
114
+ type: z.ZodLiteral<"departed">;
115
+ uid: z.ZodString;
116
+ first_name: z.ZodDefault<z.ZodString>;
117
+ last_name: z.ZodDefault<z.ZodString>;
118
+ color: z.ZodDefault<z.ZodString>;
119
+ }, z.core.$strip>], "type">>>;
120
+ shared_with_uids: z.ZodDefault<z.ZodArray<z.ZodString>>;
121
+ source_image: z.ZodOptional<z.ZodURL>;
122
+ receipt_status: z.ZodDefault<z.ZodEnum<{
123
+ draft: "draft";
124
+ in_progress: "in_progress";
125
+ completed: "completed";
126
+ }>>;
127
+ merchant_name: z.ZodDefault<z.ZodString>;
128
+ merchant_address: z.ZodDefault<z.ZodString>;
129
+ merchant_full_address: z.ZodDefault<z.ZodString>;
130
+ merchant_city: z.ZodDefault<z.ZodString>;
131
+ merchant_state: z.ZodDefault<z.ZodString>;
132
+ merchant_google_maps_uri: z.ZodOptional<z.ZodURL>;
133
+ original_upload_time: z.ZodDefault<z.ZodCoercedDate<unknown>>;
134
+ transaction_date_time: z.ZodDefault<z.ZodCoercedDate<unknown>>;
135
+ server_name: z.ZodDefault<z.ZodString>;
136
+ guest_count: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
137
+ currency_code: z.ZodDefault<z.ZodString>;
138
+ subtotal: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
139
+ tax: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
140
+ tip: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
141
+ additional_tip: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
142
+ service_fee: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
143
+ discount: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
144
+ grand_total: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
145
+ card_issuer: z.ZodOptional<z.ZodString>;
146
+ card_id: z.ZodOptional<z.ZodString>;
147
+ deleted_at: z.ZodOptional<z.ZodCoercedDate<unknown>>;
148
+ }, z.core.$strip>;
149
+ type Receipt = z.infer<typeof receiptSchema>;
150
+ type SharedWithEntry = Receipt["shared_with"][number];
151
+ /**
152
+ * READ schema for receipt documents. `receiptSchema` answers "may this be
153
+ * submitted as finished?"; this answers "can we render what's actually
154
+ * stored?" Completion rules are relaxed because incomplete docs exist by
155
+ * design (drafts, best-effort extraction); structural rules (types, ranges,
156
+ * enums) still apply, so genuine corruption is skipped rather than rendered.
157
+ */
158
+ declare const receiptReadSchema: z.ZodObject<{
159
+ receipt_id: z.ZodString;
160
+ owner_id: z.ZodString;
161
+ shared_with: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
162
+ type: z.ZodDefault<z.ZodLiteral<"member">>;
163
+ uid: z.ZodString;
164
+ }, z.core.$strip>, z.ZodObject<{
165
+ type: z.ZodDefault<z.ZodLiteral<"guest">>;
166
+ uid: z.ZodUUID;
167
+ first_name: z.ZodDefault<z.ZodString>;
168
+ last_name: z.ZodDefault<z.ZodOptional<z.ZodString>>;
169
+ emoji: z.ZodDefault<z.ZodOptional<z.ZodString>>;
170
+ venmo: z.ZodDefault<z.ZodOptional<z.ZodString>>;
171
+ cashapp: z.ZodDefault<z.ZodOptional<z.ZodString>>;
172
+ phone_number: z.ZodDefault<z.ZodOptional<z.ZodString>>;
173
+ color: z.ZodString;
174
+ }, z.core.$strip>, z.ZodObject<{
175
+ type: z.ZodLiteral<"departed">;
176
+ uid: z.ZodString;
177
+ first_name: z.ZodDefault<z.ZodString>;
178
+ last_name: z.ZodDefault<z.ZodString>;
179
+ color: z.ZodDefault<z.ZodString>;
180
+ }, z.core.$strip>], "type">>>;
181
+ shared_with_uids: z.ZodDefault<z.ZodArray<z.ZodString>>;
182
+ source_image: z.ZodOptional<z.ZodURL>;
183
+ receipt_status: z.ZodDefault<z.ZodEnum<{
184
+ draft: "draft";
185
+ in_progress: "in_progress";
186
+ completed: "completed";
187
+ }>>;
188
+ merchant_address: z.ZodDefault<z.ZodString>;
189
+ merchant_full_address: z.ZodDefault<z.ZodString>;
190
+ merchant_city: z.ZodDefault<z.ZodString>;
191
+ merchant_state: z.ZodDefault<z.ZodString>;
192
+ merchant_google_maps_uri: z.ZodOptional<z.ZodURL>;
193
+ original_upload_time: z.ZodDefault<z.ZodCoercedDate<unknown>>;
194
+ transaction_date_time: z.ZodDefault<z.ZodCoercedDate<unknown>>;
195
+ server_name: z.ZodDefault<z.ZodString>;
196
+ guest_count: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
197
+ currency_code: z.ZodDefault<z.ZodString>;
198
+ subtotal: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
199
+ tax: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
200
+ tip: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
201
+ additional_tip: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
202
+ service_fee: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
203
+ discount: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
204
+ grand_total: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
205
+ card_issuer: z.ZodOptional<z.ZodString>;
206
+ card_id: z.ZodOptional<z.ZodString>;
207
+ deleted_at: z.ZodOptional<z.ZodCoercedDate<unknown>>;
208
+ merchant_name: z.ZodDefault<z.ZodString>;
209
+ }, z.core.$strip>;
210
+ //#endregion
211
+ //#region src/split.d.ts
212
+ declare const splitResultSchema: z.ZodObject<{
213
+ users: z.ZodArray<z.ZodObject<{
214
+ uid: z.ZodString;
215
+ items: z.ZodArray<z.ZodObject<{
216
+ item_id: z.ZodUUID;
217
+ item_name: z.ZodString;
218
+ item_split_price: z.ZodNumber;
219
+ }, z.core.$strip>>;
220
+ subtotal: z.ZodNumber;
221
+ tax: z.ZodNumber;
222
+ tip: z.ZodNumber;
223
+ additional_tip: z.ZodNumber;
224
+ service_fee: z.ZodNumber;
225
+ discount: z.ZodNumber;
226
+ total: z.ZodNumber;
227
+ }, z.core.$strip>>;
228
+ }, z.core.$strip>;
229
+ type SplitResult = z.infer<typeof splitResultSchema>;
230
+ //#endregion
231
+ //#region src/extraction.d.ts
232
+ declare const extractedItemSchema: z.ZodObject<{
233
+ item_name: z.ZodString;
234
+ item_quantity: z.ZodInt;
235
+ item_unit_price: z.ZodNumber;
236
+ item_total_price: z.ZodNumber;
237
+ item_modifiers: z.ZodArray<z.ZodObject<{
238
+ modifier_description: z.ZodString;
239
+ }, z.core.$strip>>;
240
+ }, z.core.$strip>;
241
+ type ExtractedItem = z.infer<typeof extractedItemSchema>;
242
+ declare const receiptExtractionSchema: z.ZodObject<{
243
+ merchant_name: z.ZodString;
244
+ merchant_address: z.ZodString;
245
+ merchant_city: z.ZodOptional<z.ZodString>;
246
+ merchant_state: z.ZodOptional<z.ZodString>;
247
+ transaction_date_time: z.ZodString;
248
+ server_name: z.ZodString;
249
+ guest_count: z.ZodInt;
250
+ currency_code: z.ZodString;
251
+ items: z.ZodArray<z.ZodObject<{
252
+ item_name: z.ZodString;
253
+ item_quantity: z.ZodInt;
254
+ item_unit_price: z.ZodNumber;
255
+ item_total_price: z.ZodNumber;
256
+ item_modifiers: z.ZodArray<z.ZodObject<{
257
+ modifier_description: z.ZodString;
258
+ }, z.core.$strip>>;
259
+ }, z.core.$strip>>;
260
+ subtotal: z.ZodNumber;
261
+ tax: z.ZodNumber;
262
+ tip: z.ZodNumber;
263
+ additional_tip: z.ZodOptional<z.ZodNumber>;
264
+ service_fee: z.ZodOptional<z.ZodNumber>;
265
+ discount: z.ZodOptional<z.ZodNumber>;
266
+ grand_total: z.ZodNumber;
267
+ card_issuer: z.ZodOptional<z.ZodString>;
268
+ card_id: z.ZodOptional<z.ZodString>;
269
+ }, z.core.$strip>;
270
+ type ReceiptExtraction = z.infer<typeof receiptExtractionSchema>;
271
+ //#endregion
272
+ //#region src/sanitize.d.ts
273
+ /**
274
+ * A 2-letter uppercase state/region code, or "". Truncating a full name would
275
+ * fabricate a wrong code ("Texas" → "TE"; "Arizona" → "AR", which is
276
+ * Arkansas), so anything that isn't already a 2-letter code becomes "".
277
+ */
278
+ declare const stateCodeSchema: z.ZodPipe<z.ZodPipe<z.ZodCatch<z.ZodString>, z.ZodTransform<string, string>>, z.ZodTransform<string, string>>;
279
+ /**
280
+ * Maps one extracted line item (plus the caller-supplied identity fields) to
281
+ * exactly the item document shape consumers store and read, with every value
282
+ * forced into contract bounds. Output always satisfies `ItemRead`.
283
+ *
284
+ * Usage: `sanitizedItemSchema.parse({ ...rawExtractedItem, item_id, item_order })`
285
+ */
286
+ declare const sanitizedItemSchema: z.ZodObject<{
287
+ item_id: z.ZodUUID;
288
+ item_order: z.ZodCatch<z.ZodNumber>;
289
+ item_name: z.ZodPipe<z.ZodCatch<z.ZodString>, z.ZodTransform<string, string>>;
290
+ item_note: z.ZodDefault<z.ZodCatch<z.ZodString>>;
291
+ item_quantity: z.ZodPipe<z.ZodCatch<z.ZodNumber>, z.ZodTransform<number, number>>;
292
+ item_unit_price: z.ZodPipe<z.ZodCatch<z.ZodNumber>, z.ZodTransform<number, number>>;
293
+ item_total_price: z.ZodPipe<z.ZodCatch<z.ZodNumber>, z.ZodTransform<number, number>>;
294
+ item_modifiers: z.ZodPipe<z.ZodCatch<z.ZodArray<z.ZodCatch<z.ZodObject<{
295
+ modifier_description: z.ZodPipe<z.ZodCatch<z.ZodString>, z.ZodTransform<string, string>>;
296
+ }, z.core.$strip>>>>, z.ZodTransform<{
297
+ modifier_description: string;
298
+ }[], {
299
+ modifier_description: string;
300
+ }[]>>;
301
+ item_assigned_uids: z.ZodDefault<z.ZodCatch<z.ZodArray<z.ZodString>>>;
302
+ }, z.core.$strip>;
303
+ type SanitizedItem = z.infer<typeof sanitizedItemSchema>;
304
+ //#endregion
305
+ //#region src/gemini.d.ts
306
+ declare const GEMINI_SUPPORTED_KEYWORDS: Set<string>;
307
+ type JsonObject = Record<string, unknown>;
308
+ /**
309
+ * The Gemini `responseJsonSchema` value for a zod schema. Feed the result to
310
+ * `generateContent`'s config alongside `responseMimeType: "application/json"`.
311
+ */
312
+ declare function toGeminiSchema(schema: z.ZodType): JsonObject;
313
+ //#endregion
314
+ export { DEFAULT_AVATAR_COLOR, DepartedRef, ExtractedItem, GEMINI_SUPPORTED_KEYWORDS, Guest, GuestInput, Item, ItemModifier, ItemRead, MAX_ITEM_PRICE, MAX_ITEM_QUANTITY, MAX_ITEM_TOTAL_PRICE, ProfileRef, RECEIPTS_COLLECTION, RECEIPT_ITEMS_COLLECTION, Receipt, ReceiptExtraction, SanitizedItem, SharedWithEntry, SplitResult, departedRefSchema, extractedItemSchema, guestSchema, itemModifierSchema, itemReadSchema, itemSchema, profileRefSchema, receiptExtractionSchema, receiptReadSchema, receiptSchema, sanitizedItemSchema, splitResultSchema, stateCodeSchema, toGeminiSchema };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,314 @@
1
- //#region src/index.d.ts
2
- declare const SCHEMA_PACKAGE_NAME = "@divythebill/schema";
1
+ import { z } from "zod";
2
+ //#region src/constants.d.ts
3
+ /** Maximum quantity allowed for a single line item. */
4
+ declare const MAX_ITEM_QUANTITY = 999;
5
+ /** Maximum magnitude (absolute value) allowed for an item's unit price. */
6
+ declare const MAX_ITEM_PRICE = 1000000;
7
+ /**
8
+ * Upper bound for an item's total price. Derived from the unit-price and
9
+ * quantity caps (total = unit × quantity), so valid input can never exceed it.
10
+ */
11
+ declare const MAX_ITEM_TOTAL_PRICE: number;
12
+ /** Fallback avatar/profile color used when a user hasn't picked one. */
13
+ declare const DEFAULT_AVATAR_COLOR = "#bfdbfe";
14
+ /** Collection holding receipt documents. */
15
+ declare const RECEIPTS_COLLECTION = "receipts";
16
+ /** Subcollection name for a receipt's line items. */
17
+ declare const RECEIPT_ITEMS_COLLECTION = "items";
3
18
  //#endregion
4
- export { SCHEMA_PACKAGE_NAME };
19
+ //#region src/itemModifier.d.ts
20
+ declare const itemModifierSchema: z.ZodObject<{
21
+ modifier_description: z.ZodDefault<z.ZodString>;
22
+ }, z.core.$strip>;
23
+ type ItemModifier = z.infer<typeof itemModifierSchema>;
24
+ //#endregion
25
+ //#region src/item.d.ts
26
+ declare const itemSchema: z.ZodObject<{
27
+ item_id: z.ZodUUID;
28
+ item_name: z.ZodString;
29
+ item_note: z.ZodDefault<z.ZodOptional<z.ZodString>>;
30
+ item_quantity: z.ZodInt;
31
+ item_unit_price: z.ZodNumber;
32
+ item_total_price: z.ZodNumber;
33
+ item_modifiers: z.ZodDefault<z.ZodArray<z.ZodObject<{
34
+ modifier_description: z.ZodDefault<z.ZodString>;
35
+ }, z.core.$strip>>>;
36
+ item_assigned_uids: z.ZodDefault<z.ZodArray<z.ZodString>>;
37
+ item_order: z.ZodDefault<z.ZodNumber>;
38
+ }, z.core.$strip>;
39
+ type Item = z.infer<typeof itemSchema>;
40
+ /**
41
+ * READ schema for item documents. `itemSchema` answers "may this be submitted
42
+ * as finished?" (form validation); this answers "can we render what's actually
43
+ * stored?" So the name may be empty: a just-added item is written unnamed
44
+ * while it's being edited, and the read layer must still surface it.
45
+ */
46
+ declare const itemReadSchema: z.ZodObject<{
47
+ item_id: z.ZodUUID;
48
+ item_note: z.ZodDefault<z.ZodOptional<z.ZodString>>;
49
+ item_quantity: z.ZodInt;
50
+ item_unit_price: z.ZodNumber;
51
+ item_total_price: z.ZodNumber;
52
+ item_modifiers: z.ZodDefault<z.ZodArray<z.ZodObject<{
53
+ modifier_description: z.ZodDefault<z.ZodString>;
54
+ }, z.core.$strip>>>;
55
+ item_assigned_uids: z.ZodDefault<z.ZodArray<z.ZodString>>;
56
+ item_order: z.ZodDefault<z.ZodNumber>;
57
+ item_name: z.ZodDefault<z.ZodString>;
58
+ }, z.core.$strip>;
59
+ /**
60
+ * The lenient shape of an item as stored / displayed — the display and
61
+ * in-progress-draft type. `Item` (from `itemSchema`) is the strict, validated
62
+ * write shape; a draft is narrowed to `Item` via `itemSchema` only at commit.
63
+ * Everything that reads or edits uses `ItemRead`; everything that *writes*
64
+ * takes `Item`.
65
+ */
66
+ type ItemRead = z.infer<typeof itemReadSchema>;
67
+ //#endregion
68
+ //#region src/participant.d.ts
69
+ declare const guestSchema: z.ZodObject<{
70
+ type: z.ZodDefault<z.ZodLiteral<"guest">>;
71
+ uid: z.ZodUUID;
72
+ first_name: z.ZodDefault<z.ZodString>;
73
+ last_name: z.ZodDefault<z.ZodOptional<z.ZodString>>;
74
+ emoji: z.ZodDefault<z.ZodOptional<z.ZodString>>;
75
+ venmo: z.ZodDefault<z.ZodOptional<z.ZodString>>;
76
+ cashapp: z.ZodDefault<z.ZodOptional<z.ZodString>>;
77
+ phone_number: z.ZodDefault<z.ZodOptional<z.ZodString>>;
78
+ color: z.ZodString;
79
+ }, z.core.$strip>;
80
+ type Guest = z.infer<typeof guestSchema>;
81
+ type GuestInput = z.input<typeof guestSchema>;
82
+ declare const profileRefSchema: z.ZodObject<{
83
+ type: z.ZodDefault<z.ZodLiteral<"member">>;
84
+ uid: z.ZodString;
85
+ }, z.core.$strip>;
86
+ type ProfileRef = z.infer<typeof profileRefSchema>;
87
+ declare const departedRefSchema: z.ZodObject<{
88
+ type: z.ZodLiteral<"departed">;
89
+ uid: z.ZodString;
90
+ first_name: z.ZodDefault<z.ZodString>;
91
+ last_name: z.ZodDefault<z.ZodString>;
92
+ color: z.ZodDefault<z.ZodString>;
93
+ }, z.core.$strip>;
94
+ type DepartedRef = z.infer<typeof departedRefSchema>;
95
+ //#endregion
96
+ //#region src/receipt.d.ts
97
+ declare const receiptSchema: z.ZodObject<{
98
+ receipt_id: z.ZodString;
99
+ owner_id: z.ZodString;
100
+ shared_with: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
101
+ type: z.ZodDefault<z.ZodLiteral<"member">>;
102
+ uid: z.ZodString;
103
+ }, z.core.$strip>, z.ZodObject<{
104
+ type: z.ZodDefault<z.ZodLiteral<"guest">>;
105
+ uid: z.ZodUUID;
106
+ first_name: z.ZodDefault<z.ZodString>;
107
+ last_name: z.ZodDefault<z.ZodOptional<z.ZodString>>;
108
+ emoji: z.ZodDefault<z.ZodOptional<z.ZodString>>;
109
+ venmo: z.ZodDefault<z.ZodOptional<z.ZodString>>;
110
+ cashapp: z.ZodDefault<z.ZodOptional<z.ZodString>>;
111
+ phone_number: z.ZodDefault<z.ZodOptional<z.ZodString>>;
112
+ color: z.ZodString;
113
+ }, z.core.$strip>, z.ZodObject<{
114
+ type: z.ZodLiteral<"departed">;
115
+ uid: z.ZodString;
116
+ first_name: z.ZodDefault<z.ZodString>;
117
+ last_name: z.ZodDefault<z.ZodString>;
118
+ color: z.ZodDefault<z.ZodString>;
119
+ }, z.core.$strip>], "type">>>;
120
+ shared_with_uids: z.ZodDefault<z.ZodArray<z.ZodString>>;
121
+ source_image: z.ZodOptional<z.ZodURL>;
122
+ receipt_status: z.ZodDefault<z.ZodEnum<{
123
+ draft: "draft";
124
+ in_progress: "in_progress";
125
+ completed: "completed";
126
+ }>>;
127
+ merchant_name: z.ZodDefault<z.ZodString>;
128
+ merchant_address: z.ZodDefault<z.ZodString>;
129
+ merchant_full_address: z.ZodDefault<z.ZodString>;
130
+ merchant_city: z.ZodDefault<z.ZodString>;
131
+ merchant_state: z.ZodDefault<z.ZodString>;
132
+ merchant_google_maps_uri: z.ZodOptional<z.ZodURL>;
133
+ original_upload_time: z.ZodDefault<z.ZodCoercedDate<unknown>>;
134
+ transaction_date_time: z.ZodDefault<z.ZodCoercedDate<unknown>>;
135
+ server_name: z.ZodDefault<z.ZodString>;
136
+ guest_count: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
137
+ currency_code: z.ZodDefault<z.ZodString>;
138
+ subtotal: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
139
+ tax: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
140
+ tip: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
141
+ additional_tip: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
142
+ service_fee: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
143
+ discount: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
144
+ grand_total: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
145
+ card_issuer: z.ZodOptional<z.ZodString>;
146
+ card_id: z.ZodOptional<z.ZodString>;
147
+ deleted_at: z.ZodOptional<z.ZodCoercedDate<unknown>>;
148
+ }, z.core.$strip>;
149
+ type Receipt = z.infer<typeof receiptSchema>;
150
+ type SharedWithEntry = Receipt["shared_with"][number];
151
+ /**
152
+ * READ schema for receipt documents. `receiptSchema` answers "may this be
153
+ * submitted as finished?"; this answers "can we render what's actually
154
+ * stored?" Completion rules are relaxed because incomplete docs exist by
155
+ * design (drafts, best-effort extraction); structural rules (types, ranges,
156
+ * enums) still apply, so genuine corruption is skipped rather than rendered.
157
+ */
158
+ declare const receiptReadSchema: z.ZodObject<{
159
+ receipt_id: z.ZodString;
160
+ owner_id: z.ZodString;
161
+ shared_with: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
162
+ type: z.ZodDefault<z.ZodLiteral<"member">>;
163
+ uid: z.ZodString;
164
+ }, z.core.$strip>, z.ZodObject<{
165
+ type: z.ZodDefault<z.ZodLiteral<"guest">>;
166
+ uid: z.ZodUUID;
167
+ first_name: z.ZodDefault<z.ZodString>;
168
+ last_name: z.ZodDefault<z.ZodOptional<z.ZodString>>;
169
+ emoji: z.ZodDefault<z.ZodOptional<z.ZodString>>;
170
+ venmo: z.ZodDefault<z.ZodOptional<z.ZodString>>;
171
+ cashapp: z.ZodDefault<z.ZodOptional<z.ZodString>>;
172
+ phone_number: z.ZodDefault<z.ZodOptional<z.ZodString>>;
173
+ color: z.ZodString;
174
+ }, z.core.$strip>, z.ZodObject<{
175
+ type: z.ZodLiteral<"departed">;
176
+ uid: z.ZodString;
177
+ first_name: z.ZodDefault<z.ZodString>;
178
+ last_name: z.ZodDefault<z.ZodString>;
179
+ color: z.ZodDefault<z.ZodString>;
180
+ }, z.core.$strip>], "type">>>;
181
+ shared_with_uids: z.ZodDefault<z.ZodArray<z.ZodString>>;
182
+ source_image: z.ZodOptional<z.ZodURL>;
183
+ receipt_status: z.ZodDefault<z.ZodEnum<{
184
+ draft: "draft";
185
+ in_progress: "in_progress";
186
+ completed: "completed";
187
+ }>>;
188
+ merchant_address: z.ZodDefault<z.ZodString>;
189
+ merchant_full_address: z.ZodDefault<z.ZodString>;
190
+ merchant_city: z.ZodDefault<z.ZodString>;
191
+ merchant_state: z.ZodDefault<z.ZodString>;
192
+ merchant_google_maps_uri: z.ZodOptional<z.ZodURL>;
193
+ original_upload_time: z.ZodDefault<z.ZodCoercedDate<unknown>>;
194
+ transaction_date_time: z.ZodDefault<z.ZodCoercedDate<unknown>>;
195
+ server_name: z.ZodDefault<z.ZodString>;
196
+ guest_count: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
197
+ currency_code: z.ZodDefault<z.ZodString>;
198
+ subtotal: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
199
+ tax: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
200
+ tip: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
201
+ additional_tip: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
202
+ service_fee: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
203
+ discount: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
204
+ grand_total: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
205
+ card_issuer: z.ZodOptional<z.ZodString>;
206
+ card_id: z.ZodOptional<z.ZodString>;
207
+ deleted_at: z.ZodOptional<z.ZodCoercedDate<unknown>>;
208
+ merchant_name: z.ZodDefault<z.ZodString>;
209
+ }, z.core.$strip>;
210
+ //#endregion
211
+ //#region src/split.d.ts
212
+ declare const splitResultSchema: z.ZodObject<{
213
+ users: z.ZodArray<z.ZodObject<{
214
+ uid: z.ZodString;
215
+ items: z.ZodArray<z.ZodObject<{
216
+ item_id: z.ZodUUID;
217
+ item_name: z.ZodString;
218
+ item_split_price: z.ZodNumber;
219
+ }, z.core.$strip>>;
220
+ subtotal: z.ZodNumber;
221
+ tax: z.ZodNumber;
222
+ tip: z.ZodNumber;
223
+ additional_tip: z.ZodNumber;
224
+ service_fee: z.ZodNumber;
225
+ discount: z.ZodNumber;
226
+ total: z.ZodNumber;
227
+ }, z.core.$strip>>;
228
+ }, z.core.$strip>;
229
+ type SplitResult = z.infer<typeof splitResultSchema>;
230
+ //#endregion
231
+ //#region src/extraction.d.ts
232
+ declare const extractedItemSchema: z.ZodObject<{
233
+ item_name: z.ZodString;
234
+ item_quantity: z.ZodInt;
235
+ item_unit_price: z.ZodNumber;
236
+ item_total_price: z.ZodNumber;
237
+ item_modifiers: z.ZodArray<z.ZodObject<{
238
+ modifier_description: z.ZodString;
239
+ }, z.core.$strip>>;
240
+ }, z.core.$strip>;
241
+ type ExtractedItem = z.infer<typeof extractedItemSchema>;
242
+ declare const receiptExtractionSchema: z.ZodObject<{
243
+ merchant_name: z.ZodString;
244
+ merchant_address: z.ZodString;
245
+ merchant_city: z.ZodOptional<z.ZodString>;
246
+ merchant_state: z.ZodOptional<z.ZodString>;
247
+ transaction_date_time: z.ZodString;
248
+ server_name: z.ZodString;
249
+ guest_count: z.ZodInt;
250
+ currency_code: z.ZodString;
251
+ items: z.ZodArray<z.ZodObject<{
252
+ item_name: z.ZodString;
253
+ item_quantity: z.ZodInt;
254
+ item_unit_price: z.ZodNumber;
255
+ item_total_price: z.ZodNumber;
256
+ item_modifiers: z.ZodArray<z.ZodObject<{
257
+ modifier_description: z.ZodString;
258
+ }, z.core.$strip>>;
259
+ }, z.core.$strip>>;
260
+ subtotal: z.ZodNumber;
261
+ tax: z.ZodNumber;
262
+ tip: z.ZodNumber;
263
+ additional_tip: z.ZodOptional<z.ZodNumber>;
264
+ service_fee: z.ZodOptional<z.ZodNumber>;
265
+ discount: z.ZodOptional<z.ZodNumber>;
266
+ grand_total: z.ZodNumber;
267
+ card_issuer: z.ZodOptional<z.ZodString>;
268
+ card_id: z.ZodOptional<z.ZodString>;
269
+ }, z.core.$strip>;
270
+ type ReceiptExtraction = z.infer<typeof receiptExtractionSchema>;
271
+ //#endregion
272
+ //#region src/sanitize.d.ts
273
+ /**
274
+ * A 2-letter uppercase state/region code, or "". Truncating a full name would
275
+ * fabricate a wrong code ("Texas" → "TE"; "Arizona" → "AR", which is
276
+ * Arkansas), so anything that isn't already a 2-letter code becomes "".
277
+ */
278
+ declare const stateCodeSchema: z.ZodPipe<z.ZodPipe<z.ZodCatch<z.ZodString>, z.ZodTransform<string, string>>, z.ZodTransform<string, string>>;
279
+ /**
280
+ * Maps one extracted line item (plus the caller-supplied identity fields) to
281
+ * exactly the item document shape consumers store and read, with every value
282
+ * forced into contract bounds. Output always satisfies `ItemRead`.
283
+ *
284
+ * Usage: `sanitizedItemSchema.parse({ ...rawExtractedItem, item_id, item_order })`
285
+ */
286
+ declare const sanitizedItemSchema: z.ZodObject<{
287
+ item_id: z.ZodUUID;
288
+ item_order: z.ZodCatch<z.ZodNumber>;
289
+ item_name: z.ZodPipe<z.ZodCatch<z.ZodString>, z.ZodTransform<string, string>>;
290
+ item_note: z.ZodDefault<z.ZodCatch<z.ZodString>>;
291
+ item_quantity: z.ZodPipe<z.ZodCatch<z.ZodNumber>, z.ZodTransform<number, number>>;
292
+ item_unit_price: z.ZodPipe<z.ZodCatch<z.ZodNumber>, z.ZodTransform<number, number>>;
293
+ item_total_price: z.ZodPipe<z.ZodCatch<z.ZodNumber>, z.ZodTransform<number, number>>;
294
+ item_modifiers: z.ZodPipe<z.ZodCatch<z.ZodArray<z.ZodCatch<z.ZodObject<{
295
+ modifier_description: z.ZodPipe<z.ZodCatch<z.ZodString>, z.ZodTransform<string, string>>;
296
+ }, z.core.$strip>>>>, z.ZodTransform<{
297
+ modifier_description: string;
298
+ }[], {
299
+ modifier_description: string;
300
+ }[]>>;
301
+ item_assigned_uids: z.ZodDefault<z.ZodCatch<z.ZodArray<z.ZodString>>>;
302
+ }, z.core.$strip>;
303
+ type SanitizedItem = z.infer<typeof sanitizedItemSchema>;
304
+ //#endregion
305
+ //#region src/gemini.d.ts
306
+ declare const GEMINI_SUPPORTED_KEYWORDS: Set<string>;
307
+ type JsonObject = Record<string, unknown>;
308
+ /**
309
+ * The Gemini `responseJsonSchema` value for a zod schema. Feed the result to
310
+ * `generateContent`'s config alongside `responseMimeType: "application/json"`.
311
+ */
312
+ declare function toGeminiSchema(schema: z.ZodType): JsonObject;
313
+ //#endregion
314
+ export { DEFAULT_AVATAR_COLOR, DepartedRef, ExtractedItem, GEMINI_SUPPORTED_KEYWORDS, Guest, GuestInput, Item, ItemModifier, ItemRead, MAX_ITEM_PRICE, MAX_ITEM_QUANTITY, MAX_ITEM_TOTAL_PRICE, ProfileRef, RECEIPTS_COLLECTION, RECEIPT_ITEMS_COLLECTION, Receipt, ReceiptExtraction, SanitizedItem, SharedWithEntry, SplitResult, departedRefSchema, extractedItemSchema, guestSchema, itemModifierSchema, itemReadSchema, itemSchema, profileRefSchema, receiptExtractionSchema, receiptReadSchema, receiptSchema, sanitizedItemSchema, splitResultSchema, stateCodeSchema, toGeminiSchema };
package/dist/index.mjs CHANGED
@@ -1,4 +1,249 @@
1
- //#region src/index.ts
2
- const SCHEMA_PACKAGE_NAME = "@divythebill/schema";
1
+ import { z } from "zod";
2
+ //#region src/constants.ts
3
+ /** Maximum quantity allowed for a single line item. */
4
+ const MAX_ITEM_QUANTITY = 999;
5
+ /** Maximum magnitude (absolute value) allowed for an item's unit price. */
6
+ const MAX_ITEM_PRICE = 1e6;
7
+ /**
8
+ * Upper bound for an item's total price. Derived from the unit-price and
9
+ * quantity caps (total = unit × quantity), so valid input can never exceed it.
10
+ */
11
+ const MAX_ITEM_TOTAL_PRICE = MAX_ITEM_PRICE * 999;
12
+ /** Fallback avatar/profile color used when a user hasn't picked one. */
13
+ const DEFAULT_AVATAR_COLOR = "#bfdbfe";
14
+ /** Collection holding receipt documents. */
15
+ const RECEIPTS_COLLECTION = "receipts";
16
+ /** Subcollection name for a receipt's line items. */
17
+ const RECEIPT_ITEMS_COLLECTION = "items";
3
18
  //#endregion
4
- export { SCHEMA_PACKAGE_NAME };
19
+ //#region src/itemModifier.ts
20
+ const itemModifierSchema = z.object({ modifier_description: z.string().default("") });
21
+ //#endregion
22
+ //#region src/item.ts
23
+ const itemSchema = z.object({
24
+ item_id: z.uuid(),
25
+ item_name: z.string().min(1, "Item name is required."),
26
+ item_note: z.string().optional().default(""),
27
+ item_quantity: z.int().positive().max(999),
28
+ item_unit_price: z.number().min(-1e6).max(MAX_ITEM_PRICE),
29
+ item_total_price: z.number().min(-999e6).max(MAX_ITEM_TOTAL_PRICE),
30
+ item_modifiers: z.array(itemModifierSchema).default([]),
31
+ item_assigned_uids: z.array(z.string()).default([]),
32
+ item_order: z.number().default(0)
33
+ });
34
+ /**
35
+ * READ schema for item documents. `itemSchema` answers "may this be submitted
36
+ * as finished?" (form validation); this answers "can we render what's actually
37
+ * stored?" So the name may be empty: a just-added item is written unnamed
38
+ * while it's being edited, and the read layer must still surface it.
39
+ */
40
+ const itemReadSchema = itemSchema.extend({ item_name: z.string().default("") });
41
+ const guestSchema = z.object({
42
+ type: z.literal("guest").default("guest"),
43
+ uid: z.uuid(),
44
+ first_name: z.string().default(""),
45
+ last_name: z.string().optional().default(""),
46
+ emoji: z.string().optional().default(""),
47
+ venmo: z.string().trim().refine((val) => !val || !val.includes(" "), "Venmo handles cannot contain spaces").optional().default(""),
48
+ cashapp: z.string().trim().refine((val) => !val || !val.includes(" "), "Cashtags cannot contain spaces").optional().default(""),
49
+ phone_number: z.string().optional().default(""),
50
+ color: z.string()
51
+ }).refine((guest) => guest.first_name.trim().length > 0 || guest.emoji.length > 0, {
52
+ path: ["first_name"],
53
+ message: "First name is required."
54
+ });
55
+ const profileRefSchema = z.object({
56
+ type: z.literal("member").default("member"),
57
+ uid: z.string()
58
+ });
59
+ const departedRefSchema = z.object({
60
+ type: z.literal("departed"),
61
+ uid: z.string(),
62
+ first_name: z.string().default(""),
63
+ last_name: z.string().default(""),
64
+ color: z.string().default(DEFAULT_AVATAR_COLOR)
65
+ });
66
+ //#endregion
67
+ //#region src/receipt.ts
68
+ const receiptSchema = z.object({
69
+ receipt_id: z.string(),
70
+ owner_id: z.string(),
71
+ shared_with: z.array(z.discriminatedUnion("type", [
72
+ profileRefSchema,
73
+ guestSchema,
74
+ departedRefSchema
75
+ ])).default([]),
76
+ shared_with_uids: z.array(z.string()).default([]),
77
+ source_image: z.url().optional(),
78
+ receipt_status: z.enum([
79
+ "draft",
80
+ "in_progress",
81
+ "completed"
82
+ ]).default("draft"),
83
+ merchant_name: z.string().min(1, "Merchant name is required.").default(""),
84
+ merchant_address: z.string().default(""),
85
+ merchant_full_address: z.string().default(""),
86
+ merchant_city: z.string().default(""),
87
+ merchant_state: z.string().default(""),
88
+ merchant_google_maps_uri: z.url().optional(),
89
+ original_upload_time: z.coerce.date().default(/* @__PURE__ */ new Date()),
90
+ transaction_date_time: z.coerce.date().default(/* @__PURE__ */ new Date()),
91
+ server_name: z.string().trim().default("Not Listed"),
92
+ guest_count: z.number().int().positive().default(1).catch(1),
93
+ currency_code: z.string().default("USD"),
94
+ subtotal: z.coerce.number().default(0),
95
+ tax: z.coerce.number().default(0),
96
+ tip: z.coerce.number().default(0),
97
+ additional_tip: z.coerce.number().optional().default(0),
98
+ service_fee: z.coerce.number().optional().default(0),
99
+ discount: z.coerce.number().optional().default(0),
100
+ grand_total: z.coerce.number().default(0),
101
+ card_issuer: z.string().toUpperCase().optional(),
102
+ card_id: z.string().optional(),
103
+ deleted_at: z.coerce.date().optional()
104
+ });
105
+ /**
106
+ * READ schema for receipt documents. `receiptSchema` answers "may this be
107
+ * submitted as finished?"; this answers "can we render what's actually
108
+ * stored?" Completion rules are relaxed because incomplete docs exist by
109
+ * design (drafts, best-effort extraction); structural rules (types, ranges,
110
+ * enums) still apply, so genuine corruption is skipped rather than rendered.
111
+ */
112
+ const receiptReadSchema = receiptSchema.extend({ merchant_name: z.string().default("") });
113
+ //#endregion
114
+ //#region src/split.ts
115
+ const splitResultSchema = z.object({ users: z.array(z.object({
116
+ uid: z.string(),
117
+ items: z.array(z.object({
118
+ item_id: z.uuid(),
119
+ item_name: z.string(),
120
+ item_split_price: z.number()
121
+ })),
122
+ subtotal: z.number(),
123
+ tax: z.number(),
124
+ tip: z.number(),
125
+ additional_tip: z.number(),
126
+ service_fee: z.number(),
127
+ discount: z.number(),
128
+ total: z.number()
129
+ })) });
130
+ //#endregion
131
+ //#region src/extraction.ts
132
+ const extractedItemSchema = z.object({
133
+ item_name: z.string().describe("Name of the product, fee, or discount."),
134
+ item_quantity: z.int().describe("Unit count; integer, default 1."),
135
+ item_unit_price: z.number().describe("Price per unit, decimal; negative for discounts."),
136
+ item_total_price: z.number().describe("Line total (quantity * unit_price), decimal; negative for discounts."),
137
+ item_modifiers: z.array(z.object({ modifier_description: z.string().describe("The modifier text (e.g., 'No Onions').") })).describe("Unpriced descriptive lines belonging to this item; empty array if none.")
138
+ });
139
+ const receiptExtractionSchema = z.object({
140
+ merchant_name: z.string().describe("Establishment or vendor name."),
141
+ merchant_address: z.string().describe("Full street address as printed."),
142
+ merchant_city: z.string().optional().describe("City."),
143
+ merchant_state: z.string().optional().describe("State."),
144
+ transaction_date_time: z.string().describe("Purchase datetime, ISO 8601 (YYYY-MM-DDThh:mm:ss)."),
145
+ server_name: z.string().describe("Server's name; default 'Not Listed'."),
146
+ guest_count: z.int().describe("Number of guests; integer >= 1, default 1."),
147
+ currency_code: z.string().describe("3-letter ISO currency code (e.g., USD); default USD."),
148
+ items: z.array(extractedItemSchema).describe("Line items: products, flat per-item fees, and discounts (discounts have negative prices)."),
149
+ subtotal: z.number().describe("Items total before tax/tip/fees, decimal; equals the sum of all item_total_price."),
150
+ tax: z.number().describe("Total tax, decimal."),
151
+ tip: z.number().describe("Staff gratuity, decimal."),
152
+ additional_tip: z.number().optional().describe("Extra gratuity beyond the primary tip, decimal."),
153
+ service_fee: z.number().optional().describe("Whole-bill non-tax, non-gratuity fees, summed into one decimal."),
154
+ discount: z.number().optional().describe("Global bill-wide discount, negative decimal."),
155
+ grand_total: z.number().describe("Final amount paid, decimal."),
156
+ card_issuer: z.string().optional().describe("Card issuer shorthand (e.g., Visa, MC, Amex, Disc)."),
157
+ card_id: z.string().optional().describe("Last 4-5 digits of the first card used.")
158
+ });
159
+ //#endregion
160
+ //#region src/sanitize.ts
161
+ function clamp(value, min, max) {
162
+ return Math.min(Math.max(value, min), max);
163
+ }
164
+ /** A string no matter what: junk becomes "", and the result is trimmed. */
165
+ const lenientTrimmedString = z.string().catch("").transform((value) => value.trim());
166
+ /**
167
+ * A 2-letter uppercase state/region code, or "". Truncating a full name would
168
+ * fabricate a wrong code ("Texas" → "TE"; "Arizona" → "AR", which is
169
+ * Arkansas), so anything that isn't already a 2-letter code becomes "".
170
+ */
171
+ const stateCodeSchema = lenientTrimmedString.transform((value) => {
172
+ const state = value.toUpperCase();
173
+ return /^[A-Z]{2}$/.test(state) ? state : "";
174
+ });
175
+ /**
176
+ * Maps one extracted line item (plus the caller-supplied identity fields) to
177
+ * exactly the item document shape consumers store and read, with every value
178
+ * forced into contract bounds. Output always satisfies `ItemRead`.
179
+ *
180
+ * Usage: `sanitizedItemSchema.parse({ ...rawExtractedItem, item_id, item_order })`
181
+ */
182
+ const sanitizedItemSchema = z.object({
183
+ item_id: z.uuid(),
184
+ item_order: z.number().catch(0),
185
+ item_name: lenientTrimmedString,
186
+ item_note: z.string().catch("").default(""),
187
+ item_quantity: z.number().catch(1).transform((quantity) => clamp(Math.round(quantity), 1, 999)),
188
+ item_unit_price: z.number().catch(0).transform((price) => clamp(price, -1e6, MAX_ITEM_PRICE)),
189
+ item_total_price: z.number().catch(0).transform((price) => clamp(price, -999e6, MAX_ITEM_TOTAL_PRICE)),
190
+ item_modifiers: z.array(z.object({ modifier_description: lenientTrimmedString }).catch({ modifier_description: "" })).catch([]).transform((modifiers) => modifiers.filter((modifier) => modifier.modifier_description.length > 0)),
191
+ item_assigned_uids: z.array(z.string()).catch([]).default([])
192
+ });
193
+ //#endregion
194
+ //#region src/gemini.ts
195
+ const GEMINI_SUPPORTED_KEYWORDS = /* @__PURE__ */ new Set([
196
+ "$id",
197
+ "$defs",
198
+ "$ref",
199
+ "$anchor",
200
+ "type",
201
+ "format",
202
+ "title",
203
+ "description",
204
+ "enum",
205
+ "items",
206
+ "prefixItems",
207
+ "minItems",
208
+ "maxItems",
209
+ "minimum",
210
+ "maximum",
211
+ "anyOf",
212
+ "oneOf",
213
+ "properties",
214
+ "additionalProperties",
215
+ "required",
216
+ "propertyOrdering"
217
+ ]);
218
+ const NAME_TO_SCHEMA_KEYWORDS = ["properties", "$defs"];
219
+ const SCHEMA_VALUE_KEYWORDS = [
220
+ "items",
221
+ "additionalProperties",
222
+ "prefixItems",
223
+ "anyOf",
224
+ "oneOf"
225
+ ];
226
+ function isObject(value) {
227
+ return typeof value === "object" && value !== null && !Array.isArray(value);
228
+ }
229
+ function sanitizeSchemaNode(node) {
230
+ const result = {};
231
+ for (const [keyword, value] of Object.entries(node)) {
232
+ if (!GEMINI_SUPPORTED_KEYWORDS.has(keyword)) continue;
233
+ if (NAME_TO_SCHEMA_KEYWORDS.includes(keyword) && isObject(value)) result[keyword] = Object.fromEntries(Object.entries(value).map(([fieldName, subSchema]) => [fieldName, isObject(subSchema) ? sanitizeSchemaNode(subSchema) : subSchema]));
234
+ else if (SCHEMA_VALUE_KEYWORDS.includes(keyword)) if (Array.isArray(value)) result[keyword] = value.map((subSchema) => isObject(subSchema) ? sanitizeSchemaNode(subSchema) : subSchema);
235
+ else result[keyword] = isObject(value) ? sanitizeSchemaNode(value) : value;
236
+ else result[keyword] = value;
237
+ }
238
+ if (isObject(result.properties) && !result.propertyOrdering) result.propertyOrdering = Object.keys(result.properties);
239
+ return result;
240
+ }
241
+ /**
242
+ * The Gemini `responseJsonSchema` value for a zod schema. Feed the result to
243
+ * `generateContent`'s config alongside `responseMimeType: "application/json"`.
244
+ */
245
+ function toGeminiSchema(schema) {
246
+ return sanitizeSchemaNode(z.toJSONSchema(schema));
247
+ }
248
+ //#endregion
249
+ export { DEFAULT_AVATAR_COLOR, GEMINI_SUPPORTED_KEYWORDS, MAX_ITEM_PRICE, MAX_ITEM_QUANTITY, MAX_ITEM_TOTAL_PRICE, RECEIPTS_COLLECTION, RECEIPT_ITEMS_COLLECTION, departedRefSchema, extractedItemSchema, guestSchema, itemModifierSchema, itemReadSchema, itemSchema, profileRefSchema, receiptExtractionSchema, receiptReadSchema, receiptSchema, sanitizedItemSchema, splitResultSchema, stateCodeSchema, toGeminiSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divythebill/schema",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Internal shared validation schemas for Divy applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,13 +18,16 @@
18
18
  "types": "./dist/index.d.cts",
19
19
  "default": "./dist/index.cjs"
20
20
  }
21
- }
21
+ },
22
+ "./fixtures/*": "./tests/fixtures/*",
23
+ "./package.json": "./package.json"
22
24
  },
23
25
  "main": "./dist/index.cjs",
24
26
  "module": "./dist/index.mjs",
25
27
  "types": "./dist/index.d.cts",
26
28
  "files": [
27
- "dist"
29
+ "dist",
30
+ "tests/fixtures"
28
31
  ],
29
32
  "sideEffects": false,
30
33
  "engines": {
@@ -33,15 +36,19 @@
33
36
  "scripts": {
34
37
  "build": "tsdown src/index.ts --format esm,cjs --dts --publint",
35
38
  "typecheck": "tsc --noEmit",
36
- "prepublishOnly": "npm run build"
39
+ "test": "vitest run",
40
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
37
41
  },
38
42
  "peerDependencies": {
39
43
  "zod": "^4.4.0"
40
44
  },
41
45
  "devDependencies": {
46
+ "@types/node": "^24.13.3",
42
47
  "publint": "^0.3.21",
48
+ "schema-previous": "npm:@divythebill/schema@latest",
43
49
  "tsdown": "^0.22.8",
44
50
  "typescript": "~6.0.3",
51
+ "vitest": "^4.1.10",
45
52
  "zod": "^4.4.3"
46
53
  },
47
54
  "publishConfig": {
@@ -0,0 +1,95 @@
1
+ {
2
+ "receipts": {
3
+ "completed": {
4
+ "receipt_id": "receipt_user123_a1b2c3d4",
5
+ "owner_id": "user123",
6
+ "shared_with": [
7
+ { "type": "member", "uid": "user123" },
8
+ {
9
+ "type": "guest",
10
+ "uid": "6f9619ff-8b86-4d01-b42d-00cf4fc964ff",
11
+ "first_name": "Sam",
12
+ "last_name": "",
13
+ "emoji": "🦖",
14
+ "venmo": "sam-example",
15
+ "cashapp": "",
16
+ "phone_number": "",
17
+ "color": "#aabbcc"
18
+ },
19
+ {
20
+ "type": "departed",
21
+ "uid": "user456",
22
+ "first_name": "Alex",
23
+ "last_name": "Smith",
24
+ "color": "#112233"
25
+ }
26
+ ],
27
+ "shared_with_uids": ["user123"],
28
+ "source_image": "gs://bucket/user123/receipts/receipt_user123_a1b2c3d4.jpg",
29
+ "receipt_status": "completed",
30
+ "merchant_name": "Dunkin",
31
+ "merchant_address": "123 Main St, Austin, TX",
32
+ "merchant_full_address": "123 Main St, Austin, TX 78701, USA",
33
+ "merchant_city": "Austin",
34
+ "merchant_state": "TX",
35
+ "merchant_google_maps_uri": "https://maps.google.com/?cid=123456789",
36
+ "original_upload_time": "2026-07-15T18:04:11.000Z",
37
+ "transaction_date_time": "2026-07-15T12:31:00.000Z",
38
+ "server_name": "Casey",
39
+ "guest_count": 2,
40
+ "currency_code": "USD",
41
+ "subtotal": 10.53,
42
+ "tax": 0.87,
43
+ "tip": 2.0,
44
+ "additional_tip": 1.0,
45
+ "service_fee": 0.35,
46
+ "discount": -1.6,
47
+ "grand_total": 13.15,
48
+ "card_issuer": "VISA",
49
+ "card_id": "1234"
50
+ },
51
+ "bare-draft": {
52
+ "receipt_id": "receipt_user123_e5f6a7b8",
53
+ "owner_id": "user123",
54
+ "shared_with": [{ "type": "member", "uid": "user123" }],
55
+ "shared_with_uids": ["user123"],
56
+ "receipt_status": "draft",
57
+ "merchant_name": ""
58
+ }
59
+ },
60
+ "itemLists": {
61
+ "extracted-order": [
62
+ {
63
+ "item_id": "b3e4a5d6-7c8f-4a01-9b2c-1d3e5f7a9b0c",
64
+ "item_name": "Md Ice Orig Cof",
65
+ "item_note": "",
66
+ "item_quantity": 1,
67
+ "item_unit_price": 3.79,
68
+ "item_total_price": 3.79,
69
+ "item_modifiers": [{ "modifier_description": "Oatmilk" }],
70
+ "item_assigned_uids": ["user123"],
71
+ "item_order": 0
72
+ },
73
+ {
74
+ "item_id": "c4f5b6e7-8d9a-4b12-8c3d-2e4f6a8b0c1d",
75
+ "item_name": "Md Ice Orig Cof - $6 Meal Deal",
76
+ "item_note": "",
77
+ "item_quantity": 1,
78
+ "item_unit_price": -1.6,
79
+ "item_total_price": -1.6,
80
+ "item_modifiers": [],
81
+ "item_assigned_uids": [],
82
+ "item_order": 1
83
+ },
84
+ {
85
+ "item_id": "d5a6c7f8-9e0b-4c23-9d4e-3f5a7b9c1d2e",
86
+ "item_name": "6pc Hash Brown",
87
+ "item_quantity": 2,
88
+ "item_unit_price": 4.17,
89
+ "item_total_price": 8.34,
90
+ "item_assigned_uids": ["user123", "6f9619ff-8b86-4d01-b42d-00cf4fc964ff"],
91
+ "item_order": 2
92
+ }
93
+ ]
94
+ }
95
+ }