@divythebill/schema 0.1.1 → 0.2.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.
- package/dist/index.cjs +268 -3
- package/dist/index.d.cts +313 -3
- package/dist/index.d.mts +313 -3
- package/dist/index.mjs +248 -3
- package/package.json +6 -2
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,270 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Internal shared validation schemas for Divy applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,15 +33,19 @@
|
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsdown src/index.ts --format esm,cjs --dts --publint",
|
|
35
35
|
"typecheck": "tsc --noEmit",
|
|
36
|
-
"
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
39
40
|
"zod": "^4.4.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
43
|
+
"@types/node": "^24.13.3",
|
|
42
44
|
"publint": "^0.3.21",
|
|
45
|
+
"schema-previous": "npm:@divythebill/schema@latest",
|
|
43
46
|
"tsdown": "^0.22.8",
|
|
44
47
|
"typescript": "~6.0.3",
|
|
48
|
+
"vitest": "^4.1.10",
|
|
45
49
|
"zod": "^4.4.3"
|
|
46
50
|
},
|
|
47
51
|
"publishConfig": {
|