feat: add TallyNote local reimbursement ledger
TallyNote release / linux-x64 (push) Failing after 2m41s
TallyNote release / linux-x64 (push) Failing after 2m41s
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const expenseStatusSchema = z.enum(["unreimbursed", "reimbursed"]);
|
||||
export type ExpenseStatus = z.infer<typeof expenseStatusSchema>;
|
||||
|
||||
export const attachmentKindSchema = z.enum(["payment_proof", "invoice"]);
|
||||
export type AttachmentKind = z.infer<typeof attachmentKindSchema>;
|
||||
|
||||
export const MAX_AMOUNT_CENTS = 999_999_999_999;
|
||||
|
||||
const expenseFieldsSchema = z.object({
|
||||
paidAt: z.string().datetime({ offset: true }),
|
||||
amount: z.string().regex(/^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/).refine((value) => {
|
||||
try { amountToCents(value); return true; } catch { return false; }
|
||||
}, "金额超出允许范围"),
|
||||
note: z.string().trim().max(2000).default(""),
|
||||
}).strict();
|
||||
|
||||
const invoiceMissingReasonField = z.string().trim().max(500).nullable().optional();
|
||||
|
||||
export const expenseInputSchema = expenseFieldsSchema.extend({
|
||||
invoiceMissingReason: invoiceMissingReasonField.default(null),
|
||||
}).strict();
|
||||
|
||||
export const expenseUpdateSchema = expenseFieldsSchema.extend({
|
||||
invoiceMissingReason: invoiceMissingReasonField,
|
||||
version: z.number().int().positive(),
|
||||
}).strict();
|
||||
|
||||
export const statusUpdateSchema = z.object({
|
||||
status: expenseStatusSchema,
|
||||
version: z.number().int().positive(),
|
||||
}).strict();
|
||||
|
||||
export const versionSchema = z.object({
|
||||
version: z.number().int().positive(),
|
||||
}).strict();
|
||||
|
||||
export const attachmentDeleteSchema = versionSchema.extend({
|
||||
// Only needed when removing the final invoice. The server preserves an
|
||||
// existing reason when this field is omitted.
|
||||
invoiceMissingReason: invoiceMissingReasonField,
|
||||
}).strict();
|
||||
|
||||
export const permanentDeleteSchema = versionSchema.extend({
|
||||
password: z.string().min(1).max(512),
|
||||
});
|
||||
|
||||
export const loginSchema = z.object({
|
||||
username: z.string().trim().min(1).max(128),
|
||||
password: z.string().min(1).max(512),
|
||||
}).strict();
|
||||
|
||||
export const changePasswordSchema = z.object({
|
||||
currentPassword: z.string().min(1).max(512),
|
||||
newPassword: z.string().min(12).max(128),
|
||||
}).strict();
|
||||
|
||||
export const createAdminSchema = z.object({
|
||||
username: z.string().trim().min(3).max(64),
|
||||
displayName: z.string().trim().min(1).max(80),
|
||||
}).strict();
|
||||
|
||||
export const adminStatusSchema = z.object({
|
||||
status: z.enum(["active", "disabled"]),
|
||||
version: z.number().int().positive(),
|
||||
}).strict();
|
||||
|
||||
export const exportRequestSchema = z.union([
|
||||
z.object({
|
||||
ids: z.array(z.string().uuid()).min(1).max(5000).refine((ids) => new Set(ids).size === ids.length, "记录不能重复"),
|
||||
includeManifest: z.boolean().default(false),
|
||||
}).strict(),
|
||||
z.object({
|
||||
month: z.string().regex(/^\d{4}-(?:0[1-9]|1[0-2])$/),
|
||||
status: expenseStatusSchema,
|
||||
query: z.string().max(200).default(""),
|
||||
missingInvoice: z.boolean().default(false),
|
||||
includeManifest: z.boolean().default(false),
|
||||
}).strict(),
|
||||
]);
|
||||
|
||||
export const updateJobStatusSchema = z.enum([
|
||||
"queued",
|
||||
"downloading",
|
||||
"verifying",
|
||||
"staged",
|
||||
"backing_up",
|
||||
"applying",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
]);
|
||||
export type UpdateJobStatus = z.infer<typeof updateJobStatusSchema>;
|
||||
|
||||
/** The browser never supplies release URLs or filesystem paths. */
|
||||
export const updateApplySchema = z.object({
|
||||
version: z.string().trim().regex(/^v?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/),
|
||||
confirm: z.literal(true),
|
||||
}).strict();
|
||||
|
||||
export type ApiError = {
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
requestId: string;
|
||||
details?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export function amountToCents(value: string): number {
|
||||
const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(value);
|
||||
if (!match) throw new Error("INVALID_AMOUNT");
|
||||
const cents = Number(match[1]) * 100 + Number((match[2] ?? "").padEnd(2, "0"));
|
||||
if (!Number.isSafeInteger(cents) || cents <= 0 || cents > MAX_AMOUNT_CENTS) throw new Error("INVALID_AMOUNT");
|
||||
return cents;
|
||||
}
|
||||
|
||||
export function centsToAmount(cents: number): string {
|
||||
return (cents / 100).toFixed(2);
|
||||
}
|
||||
Reference in New Issue
Block a user