@happyvertical/smrt-expenses 0.54.2 → 0.54.4
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/README.md +8 -0
- package/agents/expense-ui.md +62 -0
- package/dist/chunks/{Expense-C1uUZbAr.js → Expense-C8HXtsJw.js} +9 -1
- package/dist/chunks/Expense-C8HXtsJw.js.map +1 -0
- package/dist/index.js +15 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +9 -8
- package/dist/smrt-knowledge.json +5 -5
- package/dist/svelte/components/ExpenseAmountInput.svelte +68 -0
- package/dist/svelte/components/ExpenseAmountInput.svelte.d.ts +15 -0
- package/dist/svelte/components/ExpenseAmountInput.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ExpenseForm.svelte +17 -2
- package/dist/svelte/components/ExpenseForm.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ExpenseList.svelte +36 -0
- package/dist/svelte/components/ExpenseList.svelte.d.ts +8 -0
- package/dist/svelte/components/ExpenseList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ExpenseReceiptCapture.svelte +52 -0
- package/dist/svelte/components/ExpenseReceiptCapture.svelte.d.ts +8 -0
- package/dist/svelte/components/ExpenseReceiptCapture.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ExpenseReviewQueue.svelte +54 -0
- package/dist/svelte/components/ExpenseReviewQueue.svelte.d.ts +8 -0
- package/dist/svelte/components/ExpenseReviewQueue.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +5 -2
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +7 -1
- package/dist/svelte/messages.d.ts +17 -0
- package/dist/svelte/messages.d.ts.map +1 -1
- package/dist/svelte/messages.js +17 -0
- package/dist/svelte/playground/ExpenseWorkflowPreview.svelte +20 -1
- package/dist/svelte/playground/ExpenseWorkflowPreview.svelte.d.ts.map +1 -1
- package/dist/svelte/types.d.ts +98 -0
- package/dist/svelte/types.d.ts.map +1 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +27 -0
- package/dist/ui.js.map +1 -1
- package/package.json +8 -8
- package/dist/chunks/Expense-C1uUZbAr.js.map +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
CameraCapture,
|
|
4
|
+
Form,
|
|
5
|
+
FormGroup,
|
|
6
|
+
Input,
|
|
7
|
+
} from '@happyvertical/smrt-ui/forms';
|
|
8
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
9
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
10
|
+
import { expenseMessages as M } from '../messages.js';
|
|
11
|
+
import type { ExpenseReceiptCaptureProps } from '../types.js';
|
|
12
|
+
/** Separate multipart receipt action; storage and attachment remain server-owned. */
|
|
13
|
+
export interface Props extends ExpenseReceiptCaptureProps {}
|
|
14
|
+
let {
|
|
15
|
+
action,
|
|
16
|
+
hiddenFields = [],
|
|
17
|
+
fileField = 'receipt',
|
|
18
|
+
fileName = 'receipt.jpg',
|
|
19
|
+
canAttach = false,
|
|
20
|
+
pending = false,
|
|
21
|
+
message,
|
|
22
|
+
intentField = 'intent',
|
|
23
|
+
intent = 'attach-receipt',
|
|
24
|
+
onsubmit,
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
const { t } = useI18n();
|
|
27
|
+
let camera = $state(false);
|
|
28
|
+
const id = $props.id();
|
|
29
|
+
</script>
|
|
30
|
+
<section class="receipt-capture" aria-label={t(M['expenses.receipt.title'])}>
|
|
31
|
+
<h3>{t(M['expenses.receipt.title'])}</h3>
|
|
32
|
+
{#if message}<p role="alert">{message}</p>{/if}
|
|
33
|
+
<p>{t(M['expenses.receipt.reselect'])}</p>
|
|
34
|
+
{#if canAttach}
|
|
35
|
+
<Form {action} method="post" enctype="multipart/form-data" preventDefault={false} {onsubmit} aria-busy={pending}>
|
|
36
|
+
{#each hiddenFields as field}<Input type="hidden" name={field.name} value={field.value} interaction={false} />{/each}
|
|
37
|
+
<Button type="button" disabled={pending} aria-pressed={camera} onclick={() => { camera = !camera; }}>{t(M['expenses.receipt.camera'])}</Button>
|
|
38
|
+
{#if camera}
|
|
39
|
+
<CameraCapture name={fileField} {fileName} disabled={pending} />
|
|
40
|
+
{:else}
|
|
41
|
+
<FormGroup id={`receipt-${id}`} label={t(M['expenses.receipt.file'])}>
|
|
42
|
+
<Input type="file" name={fileField} accept="image/*,application/pdf" disabled={pending} />
|
|
43
|
+
</FormGroup>
|
|
44
|
+
{/if}
|
|
45
|
+
<Button type="submit" name={intentField} value={intent} disabled={pending}>{t(M['expenses.receipt.save'])}</Button>
|
|
46
|
+
</Form>
|
|
47
|
+
{/if}
|
|
48
|
+
</section>
|
|
49
|
+
<style>
|
|
50
|
+
.receipt-capture { display: grid; gap: var(--smrt-spacing-3); min-width: 0; overflow-wrap: anywhere; }
|
|
51
|
+
h3, p { margin: 0; }
|
|
52
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExpenseReceiptCaptureProps } from '../types.js';
|
|
2
|
+
/** Separate multipart receipt action; storage and attachment remain server-owned. */
|
|
3
|
+
export interface Props extends ExpenseReceiptCaptureProps {
|
|
4
|
+
}
|
|
5
|
+
declare const ExpenseReceiptCapture: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type ExpenseReceiptCapture = ReturnType<typeof ExpenseReceiptCapture>;
|
|
7
|
+
export default ExpenseReceiptCapture;
|
|
8
|
+
//# sourceMappingURL=ExpenseReceiptCapture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpenseReceiptCapture.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ExpenseReceiptCapture.svelte.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAE9D,qFAAqF;AACrF,MAAM,WAAW,KAAM,SAAQ,0BAA0B;CAAG;AA+C5D,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { CurrencyDisplay } from '@happyvertical/smrt-ui';
|
|
3
|
+
import { Form, FormGroup, Input, Textarea } from '@happyvertical/smrt-ui/forms';
|
|
4
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
5
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
6
|
+
import { expenseMessages as M } from '../messages.js';
|
|
7
|
+
import type { ExpenseReviewQueueProps } from '../types.js';
|
|
8
|
+
/** Explicit native review decisions; never infer capabilities from displayed status. */
|
|
9
|
+
export interface Props extends ExpenseReviewQueueProps {}
|
|
10
|
+
let {
|
|
11
|
+
expenses,
|
|
12
|
+
canReview = false,
|
|
13
|
+
message,
|
|
14
|
+
expenseField = 'expenseId',
|
|
15
|
+
reasonField = 'reason',
|
|
16
|
+
intentField = 'intent',
|
|
17
|
+
approveIntent = 'approve',
|
|
18
|
+
rejectIntent = 'reject',
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
const { t } = useI18n();
|
|
21
|
+
const id = $props.id();
|
|
22
|
+
</script>
|
|
23
|
+
<section class="queue" aria-label={t(M['expenses.queue.title'])}>
|
|
24
|
+
<h2>{t(M['expenses.queue.title'])}</h2>
|
|
25
|
+
{#if message}<p role="alert">{message}</p>{/if}
|
|
26
|
+
{#if !expenses.length}<p>{t(M['expenses.queue.empty'])}</p>{/if}
|
|
27
|
+
{#each expenses as row, index (row.id)}
|
|
28
|
+
<article aria-label={row.description}>
|
|
29
|
+
<h3>{row.description}</h3>
|
|
30
|
+
<p><CurrencyDisplay amount={row.amount} currency={row.currency} unit="cents" /> · {row.incurredOn}</p>
|
|
31
|
+
<p>{row.statusLabel}</p>
|
|
32
|
+
{#if row.note}<p>{row.note}</p>{/if}
|
|
33
|
+
{#if row.message}<p role="alert">{row.message}</p>{/if}
|
|
34
|
+
{#if canReview && row.canReview && row.action !== undefined}
|
|
35
|
+
<Form action={row.action} method="post" preventDefault={false} aria-busy={row.pending}>
|
|
36
|
+
<Input type="hidden" name={expenseField} value={row.id} interaction={false} />
|
|
37
|
+
{#each row.hiddenFields ?? [] as field}<Input type="hidden" name={field.name} value={field.value} interaction={false} />{/each}
|
|
38
|
+
<FormGroup id={`reason-${id}-${index}`} label={t(M['expenses.queue.reason'])}>
|
|
39
|
+
<Textarea name={reasonField} value={row.reason ?? ''} required disabled={row.pending} />
|
|
40
|
+
</FormGroup>
|
|
41
|
+
<div class="actions">
|
|
42
|
+
<Button type="submit" name={intentField} value={approveIntent} formnovalidate disabled={row.pending}>{t(M['expenses.queue.approve'])}</Button>
|
|
43
|
+
<Button type="submit" name={intentField} value={rejectIntent} disabled={row.pending}>{t(M['expenses.queue.reject'])}</Button>
|
|
44
|
+
</div>
|
|
45
|
+
</Form>
|
|
46
|
+
{/if}
|
|
47
|
+
</article>
|
|
48
|
+
{/each}
|
|
49
|
+
</section>
|
|
50
|
+
<style>
|
|
51
|
+
.queue, article { display: grid; gap: var(--smrt-spacing-3); min-width: 0; overflow-wrap: anywhere; }
|
|
52
|
+
.actions { display: flex; flex-wrap: wrap; gap: var(--smrt-spacing-3); }
|
|
53
|
+
h2, h3, p { margin: 0; }
|
|
54
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExpenseReviewQueueProps } from '../types.js';
|
|
2
|
+
/** Explicit native review decisions; never infer capabilities from displayed status. */
|
|
3
|
+
export interface Props extends ExpenseReviewQueueProps {
|
|
4
|
+
}
|
|
5
|
+
declare const ExpenseReviewQueue: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type ExpenseReviewQueue = ReturnType<typeof ExpenseReviewQueue>;
|
|
7
|
+
export default ExpenseReviewQueue;
|
|
8
|
+
//# sourceMappingURL=ExpenseReviewQueue.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpenseReviewQueue.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ExpenseReviewQueue.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,wFAAwF;AACxF,MAAM,WAAW,KAAM,SAAQ,uBAAuB;CAAG;AAqDzD,QAAA,MAAM,kBAAkB,2CAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
|
package/dist/svelte/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import ExpenseForm from './components/ExpenseForm.svelte';
|
|
2
|
+
import ExpenseList from './components/ExpenseList.svelte';
|
|
3
|
+
import ExpenseReceiptCapture from './components/ExpenseReceiptCapture.svelte';
|
|
2
4
|
import ExpenseReviewPanel from './components/ExpenseReviewPanel.svelte';
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
+
import ExpenseReviewQueue from './components/ExpenseReviewQueue.svelte';
|
|
6
|
+
export type { ExpenseDraftValues, ExpenseDuplicateCandidate, ExpenseFormProps, ExpenseHistoryEntry, ExpenseListItem, ExpenseListProps, ExpenseReceiptCaptureProps, ExpenseReferenceOption, ExpenseRequestField, ExpenseReviewAction, ExpenseReviewPanelProps, ExpenseReviewQueueItem, ExpenseReviewQueueProps, ExpenseReviewSummary, } from './types.js';
|
|
7
|
+
export { ExpenseForm, ExpenseList, ExpenseReceiptCapture, ExpenseReviewPanel, ExpenseReviewQueue, };
|
|
5
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/svelte/index.ts"],"names":[],"mappings":"AAGA,OAAO,WAAW,MAAM,iCAAiC,CAAC;AAC1D,OAAO,kBAAkB,MAAM,wCAAwC,CAAC;AAExE,YAAY,EACV,kBAAkB,EAClB,yBAAyB,EACzB,gBAAgB,EAChB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/svelte/index.ts"],"names":[],"mappings":"AAGA,OAAO,WAAW,MAAM,iCAAiC,CAAC;AAC1D,OAAO,WAAW,MAAM,iCAAiC,CAAC;AAC1D,OAAO,qBAAqB,MAAM,2CAA2C,CAAC;AAC9E,OAAO,kBAAkB,MAAM,wCAAwC,CAAC;AACxE,OAAO,kBAAkB,MAAM,wCAAwC,CAAC;AAExE,YAAY,EACV,kBAAkB,EAClB,yBAAyB,EACzB,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,WAAW,EACX,WAAW,EACX,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,GACnB,CAAC"}
|
package/dist/svelte/index.js
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
import { ModuleUIRegistry } from '@happyvertical/smrt-ui/registry';
|
|
3
3
|
import { EXPENSES_MODULE_META } from '../ui.js';
|
|
4
4
|
import ExpenseForm from './components/ExpenseForm.svelte';
|
|
5
|
+
import ExpenseList from './components/ExpenseList.svelte';
|
|
6
|
+
import ExpenseReceiptCapture from './components/ExpenseReceiptCapture.svelte';
|
|
5
7
|
import ExpenseReviewPanel from './components/ExpenseReviewPanel.svelte';
|
|
6
|
-
|
|
8
|
+
import ExpenseReviewQueue from './components/ExpenseReviewQueue.svelte';
|
|
9
|
+
export { ExpenseForm, ExpenseList, ExpenseReceiptCapture, ExpenseReviewPanel, ExpenseReviewQueue, };
|
|
7
10
|
ModuleUIRegistry.registerModule(EXPENSES_MODULE_META);
|
|
8
11
|
ModuleUIRegistry.register('@happyvertical/smrt-expenses', 'expense-form', ExpenseForm);
|
|
9
12
|
ModuleUIRegistry.register('@happyvertical/smrt-expenses', 'expense-review', ExpenseReviewPanel);
|
|
13
|
+
ModuleUIRegistry.register('@happyvertical/smrt-expenses', 'expense-list', ExpenseList);
|
|
14
|
+
ModuleUIRegistry.register('@happyvertical/smrt-expenses', 'expense-receipt-capture', ExpenseReceiptCapture);
|
|
15
|
+
ModuleUIRegistry.register('@happyvertical/smrt-expenses', 'expense-review-queue', ExpenseReviewQueue);
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
export declare const expenseMessages: {
|
|
2
|
+
readonly 'expenses.form.costObjectType': "expenses.form.costObjectType";
|
|
3
|
+
readonly 'expenses.form.costObjectId': "expenses.form.costObjectId";
|
|
4
|
+
readonly 'expenses.keypad.open': "expenses.keypad.open";
|
|
5
|
+
readonly 'expenses.keypad.delete': "expenses.keypad.delete";
|
|
6
|
+
readonly 'expenses.keypad.decimal': "expenses.keypad.decimal";
|
|
7
|
+
readonly 'expenses.receipt.title': "expenses.receipt.title";
|
|
8
|
+
readonly 'expenses.receipt.camera': "expenses.receipt.camera";
|
|
9
|
+
readonly 'expenses.receipt.file': "expenses.receipt.file";
|
|
10
|
+
readonly 'expenses.receipt.save': "expenses.receipt.save";
|
|
11
|
+
readonly 'expenses.receipt.reselect': "expenses.receipt.reselect";
|
|
12
|
+
readonly 'expenses.list.title': "expenses.list.title";
|
|
13
|
+
readonly 'expenses.list.empty': "expenses.list.empty";
|
|
14
|
+
readonly 'expenses.queue.title': "expenses.queue.title";
|
|
15
|
+
readonly 'expenses.queue.empty': "expenses.queue.empty";
|
|
16
|
+
readonly 'expenses.queue.reason': "expenses.queue.reason";
|
|
17
|
+
readonly 'expenses.queue.approve': "expenses.queue.approve";
|
|
18
|
+
readonly 'expenses.queue.reject': "expenses.queue.reject";
|
|
2
19
|
readonly 'expenses.form.title': "expenses.form.title";
|
|
3
20
|
readonly 'expenses.form.amount': "expenses.form.amount";
|
|
4
21
|
readonly 'expenses.form.currency': "expenses.form.currency";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/svelte/messages.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,eAAe
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/svelte/messages.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+C1B,CAAC"}
|
package/dist/svelte/messages.js
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { defineMessages } from '@happyvertical/smrt-ui/i18n';
|
|
2
2
|
export const expenseMessages = defineMessages({
|
|
3
|
+
'expenses.form.costObjectType': 'Cost object type',
|
|
4
|
+
'expenses.form.costObjectId': 'Cost object reference',
|
|
5
|
+
'expenses.keypad.open': 'Amount keypad',
|
|
6
|
+
'expenses.keypad.delete': 'Delete last character',
|
|
7
|
+
'expenses.keypad.decimal': 'Decimal point',
|
|
8
|
+
'expenses.receipt.title': 'Attach receipt',
|
|
9
|
+
'expenses.receipt.camera': 'Use camera',
|
|
10
|
+
'expenses.receipt.file': 'Receipt file',
|
|
11
|
+
'expenses.receipt.save': 'Attach receipt',
|
|
12
|
+
'expenses.receipt.reselect': 'After a failed upload or page reload, select the file again. The server must confirm attachment before it is considered saved.',
|
|
13
|
+
'expenses.list.title': 'Expenses',
|
|
14
|
+
'expenses.list.empty': 'No expenses for this cost object.',
|
|
15
|
+
'expenses.queue.title': 'Expense review queue',
|
|
16
|
+
'expenses.queue.empty': 'No expenses awaiting review.',
|
|
17
|
+
'expenses.queue.reason': 'Rejection reason',
|
|
18
|
+
'expenses.queue.approve': 'Approve expense',
|
|
19
|
+
'expenses.queue.reject': 'Reject expense',
|
|
3
20
|
'expenses.form.title': 'Record expense',
|
|
4
21
|
'expenses.form.amount': 'Amount',
|
|
5
22
|
'expenses.form.currency': 'Currency',
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Checkbox } from '@happyvertical/smrt-ui/forms';
|
|
3
3
|
import ExpenseForm from '../components/ExpenseForm.svelte';
|
|
4
|
+
import ExpenseList from '../components/ExpenseList.svelte';
|
|
5
|
+
import ExpenseReceiptCapture from '../components/ExpenseReceiptCapture.svelte';
|
|
4
6
|
import ExpenseReviewPanel from '../components/ExpenseReviewPanel.svelte';
|
|
7
|
+
import ExpenseReviewQueue from '../components/ExpenseReviewQueue.svelte';
|
|
5
8
|
import type { ExpenseDraftValues } from '../types.js';
|
|
6
9
|
|
|
10
|
+
const costObject = { type: '@example/jobs:Job', id: 'job-a' };
|
|
11
|
+
const expenses = [
|
|
12
|
+
{
|
|
13
|
+
id: 'expense-a',
|
|
14
|
+
description: 'Supplier materials',
|
|
15
|
+
amount: 12500,
|
|
16
|
+
currency: 'CAD',
|
|
17
|
+
incurredOn: '2026-10-03',
|
|
18
|
+
statusLabel: 'Unreviewed',
|
|
19
|
+
costObjectType: costObject.type,
|
|
20
|
+
costObjectId: costObject.id,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
7
23
|
let canEdit = $state(true);
|
|
8
24
|
let correcting = $state(false);
|
|
9
25
|
let message = $state('');
|
|
@@ -32,7 +48,10 @@ function submit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
|
|
|
32
48
|
<div class="preview">
|
|
33
49
|
<Checkbox label="May edit expense" bind:checked={canEdit} />
|
|
34
50
|
<Checkbox label="Correction workflow" bind:checked={correcting} />
|
|
35
|
-
<ExpenseForm action="" {values} {canEdit} {correcting} {message} vendors={[{ id: 'vendor-a', label: 'Supplier A' }]} hiddenFields={[{ name: 'requestId', value: 'demo-expense-request' }]} onsubmit={submit} />
|
|
51
|
+
<ExpenseForm action="" {values} {costObject} {canEdit} {correcting} {message} vendors={[{ id: 'vendor-a', label: 'Supplier A' }]} hiddenFields={[{ name: 'requestId', value: 'demo-expense-request' }]} onsubmit={submit} />
|
|
52
|
+
<ExpenseList {expenses} costObjectType={costObject.type} costObjectId={costObject.id} />
|
|
53
|
+
<ExpenseReceiptCapture action="" canAttach={canEdit} hiddenFields={[{ name: 'expenseId', value: 'expense-a' }, { name: 'requestId', value: 'demo-receipt-request' }]} onsubmit={(event) => { event.preventDefault(); }} />
|
|
54
|
+
<ExpenseReviewQueue expenses={expenses} />
|
|
36
55
|
<ExpenseReviewPanel expense={{ description: 'Supplier materials', amount: 12500, currency: 'CAD', incurredOn: '2026-10-03', statusLabel: 'Unreviewed' }} duplicates={[{ id: 'expense-other', label: 'Possible matching expense', reason: 'The server found matching receipt evidence. A reviewer must decide.' }]} history={[{ id: 'correction', label: 'Description corrected', detail: 'Caller-provided correction history' }]} receipts={{ attachments: [{ id: 'receipt', name: 'Receipt.pdf', mimeType: 'application/pdf' }] }} />
|
|
37
56
|
</div>
|
|
38
57
|
<style>.preview { display: grid; gap: var(--smrt-spacing-4); }</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpenseWorkflowPreview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/playground/ExpenseWorkflowPreview.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ExpenseWorkflowPreview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/playground/ExpenseWorkflowPreview.svelte.ts"],"names":[],"mappings":"AAuEA,QAAA,MAAM,sBAAsB,2DAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
|
package/dist/svelte/types.d.ts
CHANGED
|
@@ -47,6 +47,23 @@ export interface ExpenseFormProps {
|
|
|
47
47
|
action: string;
|
|
48
48
|
/** Retained draft strings from load or the failed action response. */
|
|
49
49
|
values: ExpenseDraftValues;
|
|
50
|
+
/** Optional editable cost-object references; omit when supplied as fixed hidden fields. */
|
|
51
|
+
costObject?: {
|
|
52
|
+
type: string;
|
|
53
|
+
id: string;
|
|
54
|
+
};
|
|
55
|
+
/** Native cost-object field names. */
|
|
56
|
+
costObjectFields?: {
|
|
57
|
+
type?: string;
|
|
58
|
+
id?: string;
|
|
59
|
+
};
|
|
60
|
+
/** Server validation messages for editable cost-object references. */
|
|
61
|
+
costObjectErrors?: {
|
|
62
|
+
type?: string;
|
|
63
|
+
id?: string;
|
|
64
|
+
};
|
|
65
|
+
/** Enable the optional amount keypad; ordinary keyboard entry remains available. */
|
|
66
|
+
amountKeypad?: boolean;
|
|
50
67
|
/** Optional stable form identity for enhancement. */
|
|
51
68
|
id?: string;
|
|
52
69
|
/** Caller-localized heading, including Purchase terminology. */
|
|
@@ -149,4 +166,85 @@ export interface ExpenseReviewPanelProps {
|
|
|
149
166
|
/** Optional review-form enhancement. */
|
|
150
167
|
onsubmit?: HTMLFormAttributes['onsubmit'];
|
|
151
168
|
}
|
|
169
|
+
/** Public model projection; monetary amounts are integer minor units. */
|
|
170
|
+
export interface ExpenseListItem extends ExpenseReviewSummary {
|
|
171
|
+
/** Stable expense identity. */
|
|
172
|
+
id: string;
|
|
173
|
+
/** Qualified cost-object class name. */
|
|
174
|
+
costObjectType: string;
|
|
175
|
+
/** Bare cost-object reference. */
|
|
176
|
+
costObjectId: string;
|
|
177
|
+
/** Optional authorized detail link. */
|
|
178
|
+
href?: string;
|
|
179
|
+
}
|
|
180
|
+
/** Caller-authorized expenses filtered to one exact cost object. */
|
|
181
|
+
export interface ExpenseListProps {
|
|
182
|
+
/** Authorized rows; this presentation filter never authorizes access. */
|
|
183
|
+
expenses: readonly ExpenseListItem[];
|
|
184
|
+
/** Exact qualified cost-object class. */
|
|
185
|
+
costObjectType: string;
|
|
186
|
+
/** Exact bare cost-object reference. */
|
|
187
|
+
costObjectId: string;
|
|
188
|
+
/** Optional localized heading. */
|
|
189
|
+
title?: string;
|
|
190
|
+
/** Server load error; existing rows remain visible. */
|
|
191
|
+
message?: string;
|
|
192
|
+
}
|
|
193
|
+
/** Separate native receipt upload, composed with the shared camera. */
|
|
194
|
+
export interface ExpenseReceiptCaptureProps {
|
|
195
|
+
/** Authorized endpoint which stores and attaches the file. */
|
|
196
|
+
action: string;
|
|
197
|
+
/** Stable request identity and expense reference; never generated here. */
|
|
198
|
+
hiddenFields?: readonly ExpenseRequestField[];
|
|
199
|
+
/** Native multipart file name. */
|
|
200
|
+
fileField?: string;
|
|
201
|
+
/** Posted camera filename. */
|
|
202
|
+
fileName?: string;
|
|
203
|
+
/** Caller capability; server reauthorizes upload and attachment. */
|
|
204
|
+
canAttach?: boolean;
|
|
205
|
+
/** In-flight state; held camera files remain postable after retry. */
|
|
206
|
+
pending?: boolean;
|
|
207
|
+
/** Server denial/upload error; file must be reselected after navigation. */
|
|
208
|
+
message?: string;
|
|
209
|
+
/** Native submitter name. */
|
|
210
|
+
intentField?: string;
|
|
211
|
+
/** Native submitter value. */
|
|
212
|
+
intent?: string;
|
|
213
|
+
/** Optional native form enhancement. */
|
|
214
|
+
onsubmit?: HTMLFormAttributes['onsubmit'];
|
|
215
|
+
}
|
|
216
|
+
/** A server-authorized review row with retained row-specific action state. */
|
|
217
|
+
export interface ExpenseReviewQueueItem extends ExpenseListItem {
|
|
218
|
+
/** Authorized native review endpoint; undefined hides decisions. */
|
|
219
|
+
action?: string;
|
|
220
|
+
/** Explicit row-specific capability. */
|
|
221
|
+
canReview?: boolean;
|
|
222
|
+
/** Retained rejection reason. */
|
|
223
|
+
reason?: string;
|
|
224
|
+
/** Server validation error or uncertain-outcome message. */
|
|
225
|
+
message?: string;
|
|
226
|
+
/** Exact row-specific request identity/context. */
|
|
227
|
+
hiddenFields?: readonly ExpenseRequestField[];
|
|
228
|
+
/** In-flight state for this row. */
|
|
229
|
+
pending?: boolean;
|
|
230
|
+
}
|
|
231
|
+
/** Native approve/reject queue. The server decides allowed state transitions. */
|
|
232
|
+
export interface ExpenseReviewQueueProps {
|
|
233
|
+
/** Authorized rows, supplied in display order. */
|
|
234
|
+
expenses: readonly ExpenseReviewQueueItem[];
|
|
235
|
+
/** Global presentation capability, in addition to each row's capability. */
|
|
236
|
+
canReview?: boolean;
|
|
237
|
+
/** Server load error. */
|
|
238
|
+
message?: string;
|
|
239
|
+
/** Native expense identity field name. */
|
|
240
|
+
expenseField?: string;
|
|
241
|
+
/** Native reason field name. */
|
|
242
|
+
reasonField?: string;
|
|
243
|
+
/** Native decision submitter field name. */
|
|
244
|
+
intentField?: string;
|
|
245
|
+
/** Approval submitter value, normally mapped to Expense.review(). */
|
|
246
|
+
approveIntent?: string;
|
|
247
|
+
/** Rejection submitter value, normally mapped to Expense.reject(). */
|
|
248
|
+
rejectIntent?: string;
|
|
249
|
+
}
|
|
152
250
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/svelte/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/svelte/types.ts"],"names":[],"mappings":"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"}
|
package/dist/ui.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE9E,yDAAyD;AACzD,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE9E,yDAAyD;AACzD,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CA8C1D,CAAC;AACF,oEAAoE;AACpE,eAAO,MAAM,oBAAoB,EAAE,cAOlC,CAAC"}
|
package/dist/ui.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
//#region src/ui.ts
|
|
2
2
|
var EXPENSES_UI_SLOTS = {
|
|
3
|
+
"expense-list": {
|
|
4
|
+
id: "expense-list",
|
|
5
|
+
label: "Expense List",
|
|
6
|
+
description: "Authorized expenses per cost object",
|
|
7
|
+
icon: "file-text",
|
|
8
|
+
category: "list",
|
|
9
|
+
order: 3,
|
|
10
|
+
propsInterface: "ExpenseListProps"
|
|
11
|
+
},
|
|
12
|
+
"expense-receipt-capture": {
|
|
13
|
+
id: "expense-receipt-capture",
|
|
14
|
+
label: "Receipt Capture",
|
|
15
|
+
description: "Camera and native multipart receipt attachment",
|
|
16
|
+
icon: "file-text",
|
|
17
|
+
category: "form",
|
|
18
|
+
order: 4,
|
|
19
|
+
propsInterface: "ExpenseReceiptCaptureProps"
|
|
20
|
+
},
|
|
21
|
+
"expense-review-queue": {
|
|
22
|
+
id: "expense-review-queue",
|
|
23
|
+
label: "Expense Review Queue",
|
|
24
|
+
description: "Native approve and reject with retained reason",
|
|
25
|
+
icon: "file-text",
|
|
26
|
+
category: "list",
|
|
27
|
+
order: 5,
|
|
28
|
+
propsInterface: "ExpenseReviewQueueProps"
|
|
29
|
+
},
|
|
3
30
|
"expense-form": {
|
|
4
31
|
id: "expense-form",
|
|
5
32
|
label: "Expense Entry",
|
package/dist/ui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.js","names":[],"sources":["../src/ui.ts"],"sourcesContent":["import type { ModuleUISlot, SmrtModuleMeta } from '@happyvertical/smrt-types';\n\n/** Discoverable presentation slots owned by Expenses. */\nexport const EXPENSES_UI_SLOTS: Record<string, ModuleUISlot> = {\n 'expense-form': {\n id: 'expense-form',\n label: 'Expense Entry',\n description: 'Native retained-value purchase and expense entry',\n icon: 'file-text',\n category: 'form',\n order: 1,\n propsInterface: 'ExpenseFormProps',\n },\n 'expense-review': {\n id: 'expense-review',\n label: 'Expense Review',\n description: 'Duplicate/correction review and private receipt composition',\n icon: 'file-text',\n category: 'detail',\n order: 2,\n propsInterface: 'ExpenseReviewPanelProps',\n },\n};\n/** Module metadata consumed without importing Svelte components. */\nexport const EXPENSES_MODULE_META: SmrtModuleMeta = {\n name: '@happyvertical/smrt-expenses',\n displayName: 'Expenses',\n description: 'Expense entry, receipt and duplicate review',\n uiSlots: EXPENSES_UI_SLOTS,\n models: ['Expense', 'ExpenseReceipt'],\n collections: ['ExpenseCollection', 'ExpenseReceiptCollection'],\n};\n"],"mappings":";AAGO,IAAM,oBAAkD;CAC7D,gBAAgB;EACd,IAAI;EACJ,OAAO;EACP,aAAa;EACb,MAAM;EACN,UAAU;EACV,OAAO;EACP,gBAAgB;CAClB;CACA,kBAAkB;EAChB,IAAI;EACJ,OAAO;EACP,aAAa;EACb,MAAM;EACN,UAAU;EACV,OAAO;EACP,gBAAgB;CAClB;AACF;AAEO,IAAM,uBAAuC;CAClD,MAAM;CACN,aAAa;CACb,aAAa;CACb,SAAS;CACT,QAAQ,CAAC,WAAW,gBAAgB;CACpC,aAAa,CAAC,qBAAqB,0BAA0B;AAC/D"}
|
|
1
|
+
{"version":3,"file":"ui.js","names":[],"sources":["../src/ui.ts"],"sourcesContent":["import type { ModuleUISlot, SmrtModuleMeta } from '@happyvertical/smrt-types';\n\n/** Discoverable presentation slots owned by Expenses. */\nexport const EXPENSES_UI_SLOTS: Record<string, ModuleUISlot> = {\n 'expense-list': {\n id: 'expense-list',\n label: 'Expense List',\n description: 'Authorized expenses per cost object',\n icon: 'file-text',\n category: 'list',\n order: 3,\n propsInterface: 'ExpenseListProps',\n },\n 'expense-receipt-capture': {\n id: 'expense-receipt-capture',\n label: 'Receipt Capture',\n description: 'Camera and native multipart receipt attachment',\n icon: 'file-text',\n category: 'form',\n order: 4,\n propsInterface: 'ExpenseReceiptCaptureProps',\n },\n 'expense-review-queue': {\n id: 'expense-review-queue',\n label: 'Expense Review Queue',\n description: 'Native approve and reject with retained reason',\n icon: 'file-text',\n category: 'list',\n order: 5,\n propsInterface: 'ExpenseReviewQueueProps',\n },\n 'expense-form': {\n id: 'expense-form',\n label: 'Expense Entry',\n description: 'Native retained-value purchase and expense entry',\n icon: 'file-text',\n category: 'form',\n order: 1,\n propsInterface: 'ExpenseFormProps',\n },\n 'expense-review': {\n id: 'expense-review',\n label: 'Expense Review',\n description: 'Duplicate/correction review and private receipt composition',\n icon: 'file-text',\n category: 'detail',\n order: 2,\n propsInterface: 'ExpenseReviewPanelProps',\n },\n};\n/** Module metadata consumed without importing Svelte components. */\nexport const EXPENSES_MODULE_META: SmrtModuleMeta = {\n name: '@happyvertical/smrt-expenses',\n displayName: 'Expenses',\n description: 'Expense entry, receipt and duplicate review',\n uiSlots: EXPENSES_UI_SLOTS,\n models: ['Expense', 'ExpenseReceipt'],\n collections: ['ExpenseCollection', 'ExpenseReceiptCollection'],\n};\n"],"mappings":";AAGO,IAAM,oBAAkD;CAC7D,gBAAgB;EACd,IAAI;EACJ,OAAO;EACP,aAAa;EACb,MAAM;EACN,UAAU;EACV,OAAO;EACP,gBAAgB;CAClB;CACA,2BAA2B;EACzB,IAAI;EACJ,OAAO;EACP,aAAa;EACb,MAAM;EACN,UAAU;EACV,OAAO;EACP,gBAAgB;CAClB;CACA,wBAAwB;EACtB,IAAI;EACJ,OAAO;EACP,aAAa;EACb,MAAM;EACN,UAAU;EACV,OAAO;EACP,gBAAgB;CAClB;CACA,gBAAgB;EACd,IAAI;EACJ,OAAO;EACP,aAAa;EACb,MAAM;EACN,UAAU;EACV,OAAO;EACP,gBAAgB;CAClB;CACA,kBAAkB;EAChB,IAAI;EACJ,OAAO;EACP,aAAa;EACb,MAAM;EACN,UAAU;EACV,OAAO;EACP,gBAAgB;CAClB;AACF;AAEO,IAAM,uBAAuC;CAClD,MAAM;CACN,aAAa;CACb,aAAa;CACb,SAAS;CACT,QAAQ,CAAC,WAAW,gBAAgB;CACpC,aAAa,CAAC,qBAAqB,0BAA0B;AAC/D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-expenses",
|
|
3
|
-
"version": "0.54.
|
|
3
|
+
"version": "0.54.4",
|
|
4
4
|
"description": "Expense records for the SMRT framework - reviewed costs against a cost object, deduplicated receipts, and commitment drawdown",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@happyvertical/smrt-assets": "0.54.
|
|
37
|
-
"@happyvertical/smrt-commerce": "0.54.
|
|
38
|
-
"@happyvertical/smrt-core": "0.54.
|
|
39
|
-
"@happyvertical/smrt-tenancy": "0.54.
|
|
40
|
-
"@happyvertical/smrt-types": "0.54.
|
|
41
|
-
"@happyvertical/smrt-ui": "0.54.
|
|
36
|
+
"@happyvertical/smrt-assets": "0.54.4",
|
|
37
|
+
"@happyvertical/smrt-commerce": "0.54.4",
|
|
38
|
+
"@happyvertical/smrt-core": "0.54.4",
|
|
39
|
+
"@happyvertical/smrt-tenancy": "0.54.4",
|
|
40
|
+
"@happyvertical/smrt-types": "0.54.4",
|
|
41
|
+
"@happyvertical/smrt-ui": "0.54.4"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@happyvertical/smrt-vitest": "0.54.
|
|
44
|
+
"@happyvertical/smrt-vitest": "0.54.4",
|
|
45
45
|
"@types/node": "24.13.6",
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
47
|
"vite": "8.1.4",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Expense-C1uUZbAr.js","names":[],"sources":["../../src/validation.ts","../../src/models/Expense.ts"],"sourcesContent":["/**\n * Small, pure validators shared by the expense models.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport { ExpenseError, type ExpenseErrorCode } from './types.js';\n\n/** Lowercase kebab-case: `material`, `outside-service`. */\nexport const CATEGORY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nconst ISO_CURRENCY = /^[A-Z]{3}$/;\nconst ISO_DATE = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst QUALIFIED_CLASS = /^[^\\s:]+:[A-Za-z_$][\\w$]*$/;\nconst SHA256_HEX = /^[0-9a-f]{64}$/;\n\n/**\n * Throw unless `value` is a non-negative, JavaScript-safe integer number of\n * minor units. Money is exact: `$19.99` is `1999`, never `19.99`.\n */\nexport function assertMinorUnits(\n model: string,\n label: string,\n fieldName: string,\n value: number,\n code: ExpenseErrorCode = 'EXPENSE_INVALID',\n): void {\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new ExpenseError(\n code,\n `${model} ${label}: ${fieldName} must be a non-negative integer ` +\n `number of minor units (cents) — got ${value}. $19.99 is 1999.`,\n );\n }\n}\n\n/** Throw unless `value` is an upper-case ISO 4217 code such as `USD`. */\nexport function assertIsoCurrency(\n model: string,\n label: string,\n value: string,\n): void {\n if (!ISO_CURRENCY.test(value)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `${model} ${label}: currency must be a three-letter ISO 4217 code ` +\n `(for example 'USD'), got '${value}'.`,\n );\n }\n}\n\n/** Throw unless `value` is a real calendar date written `YYYY-MM-DD`. */\nexport function assertIsoDate(\n model: string,\n label: string,\n fieldName: string,\n value: string,\n): void {\n const match = ISO_DATE.exec(value);\n const valid =\n match !== null &&\n (() => {\n const [year, month, day] = match.slice(1).map(Number);\n const date = new Date(Date.UTC(year, month - 1, day));\n return (\n date.getUTCFullYear() === year &&\n date.getUTCMonth() === month - 1 &&\n date.getUTCDate() === day\n );\n })();\n if (!valid) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `${model} ${label}: ${fieldName} must be a calendar date written ` +\n `YYYY-MM-DD, got '${value}'.`,\n );\n }\n}\n\n/**\n * Throw unless `value` is a package-qualified class name such as\n * `@happyvertical/smrt-projects:Project`. A bare class name is ambiguous\n * (packages can share names) and is refused.\n */\nexport function assertQualifiedClassName(\n model: string,\n label: string,\n value: string,\n): void {\n if (!QUALIFIED_CLASS.test(value)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `${model} ${label}: costObjectType must be a qualified class name ` +\n `such as '@happyvertical/smrt-projects:Project', got '${value}'.`,\n );\n }\n}\n\n/** Normalize and check a SHA-256 hex digest; returns it in lower case. */\nexport function normalizeSha256(model: string, label: string, value: string) {\n const digest = String(value ?? '')\n .trim()\n .toLowerCase();\n if (!SHA256_HEX.test(digest)) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `${model} ${label}: contentSha256 must be a 64-character hex SHA-256 ` +\n 'digest of the file bytes.',\n );\n }\n return digest;\n}\n\n/**\n * Lowercase hex SHA-256 of a file's bytes — the value\n * `ExpenseReceipt.contentSha256` expects.\n *\n * @example\n * ```typescript\n * const digest = computeContentSha256(await readFile('receipt.pdf'));\n * ```\n */\nexport function computeContentSha256(data: Uint8Array | string): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Milliseconds since the epoch of a date-ish value; `null` when empty. */\nexport function instantMs(value: unknown): number | null {\n if (value === null || value === undefined || value === '') return null;\n const ms =\n value instanceof Date ? value.getTime() : new Date(String(value)).getTime();\n return Number.isNaN(ms) ? null : ms;\n}\n\n/** The slice of `SmrtObject` the natural-key identity guard needs. */\ninterface NaturalKeyed {\n id?: string | null;\n slug?: string | null;\n context?: string | null;\n readonly isPersisted: boolean;\n getSlug(): Promise<unknown>;\n requireInsertOnSave(): void;\n}\n\n/**\n * Make a save write only the row its guards checked.\n *\n * Core's natural-key save adopts any same-owner row on\n * `(tenant_id, slug, context)` and upserts it without a revision predicate,\n * and these models have no human slug. So:\n *\n * - a fresh (never loaded, never saved) instance may only INSERT\n * (`requireInsertOnSave`): naming an existing row's id is refused here, and\n * a row that appears between this check and the write makes the INSERT\n * fail rather than be adopted;\n * - an existing row is changed only through a loaded instance, whose save\n * carries core's revision compare-and-swap, and it must keep the slug and\n * context it is stored under, so the write targets that row and no other.\n *\n * @returns `true` when this save is an INSERT.\n * @throws {ExpenseError} `EXPENSE_IDENTITY_CONFLICT` when a fresh instance\n * names an existing row, or a loaded one changes its natural key.\n */\nexport async function pinNaturalKey(\n model: string,\n object: NaturalKeyed,\n persisted: Record<string, unknown> | null,\n): Promise<boolean> {\n await object.getSlug();\n if (!object.isPersisted) {\n if (persisted) {\n throw new ExpenseError(\n 'EXPENSE_IDENTITY_CONFLICT',\n `${model} ${object.id} already exists; load it and save the loaded ` +\n 'instance instead of creating one with its id.',\n );\n }\n object.requireInsertOnSave();\n return true;\n }\n if (\n persisted &&\n (String(persisted.slug ?? '') !== String(object.slug ?? '') ||\n String(persisted.context ?? '') !== String(object.context ?? ''))\n ) {\n throw new ExpenseError(\n 'EXPENSE_IDENTITY_CONFLICT',\n `${model} ${object.id}: slug and context cannot change; they would ` +\n 'point the save at a different row.',\n );\n }\n return false;\n}\n\n/** The error a refused natural-key INSERT is reported as. */\nexport function identityConflict(\n model: string,\n object: { id?: string | null; slug?: string | null },\n cause: unknown,\n): ExpenseError {\n return new ExpenseError(\n 'EXPENSE_IDENTITY_CONFLICT',\n `${model} ${object.id}: another row already uses slug ` +\n `'${object.slug}' in this tenant; a new ${model} cannot take it over.`,\n { cause },\n );\n}\n\n/**\n * The tenant a save will actually write: tenancy's `beforeSave` interceptor\n * fills an unset `tenantId` from the active tenant context, after the\n * model's own checks have run, and \"unset\" is falsy there — `''` included.\n * Compare against this, not the raw field.\n */\nexport function effectiveTenant(\n tenantId: string | null | undefined,\n): string | null {\n return tenantId || getTenantId() || null;\n}\n\n/**\n * An empty tenant id is unset, never a tenant of its own: core and tenancy\n * treat it as falsy (and real tenant ids are UUIDs), so it is stored as\n * NULL — a global row — rather than as a `''` pseudo-tenant.\n */\nexport function normalizeTenantId(\n tenantId: string | null | undefined,\n): string | null {\n return tenantId || null;\n}\n","/**\n * Expense — a cost someone incurred, against a cost object, with a review\n * state and optional reimbursement and commitment links.\n *\n * @packageDocumentation\n */\n\nimport {\n ContractCollection,\n ContractLineItemCollection,\n VendorCollection,\n} from '@happyvertical/smrt-commerce';\nimport {\n crossPackageRef,\n field,\n foreignKey,\n isUniqueViolationError,\n ObjectRegistry,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport {\n ExpenseError,\n type ExpenseOptions,\n type ExpensePayer,\n type ExpenseReviewStatus,\n type MarkDuplicateExpenseInput,\n type MarkReimbursedInput,\n type RejectExpenseInput,\n type ReopenExpenseInput,\n type ReviewExpenseInput,\n} from '../types.js';\nimport {\n assertIsoCurrency,\n assertIsoDate,\n assertMinorUnits,\n assertQualifiedClassName,\n CATEGORY_PATTERN,\n effectiveTenant,\n identityConflict,\n instantMs,\n normalizeTenantId,\n pinNaturalKey,\n} from '../validation.js';\n\nconst PROFILE = '@happyvertical/smrt-profiles:Profile';\n\n/**\n * Legal review moves, keyed by the persisted status. Only the dedicated\n * methods perform them; `save()` refuses a review-field change they did not\n * authorize.\n */\nconst REVIEW_TRANSITIONS: Record<\n 'review' | 'reject' | 'markDuplicate' | 'reopen',\n { from: readonly ExpenseReviewStatus[]; to: ExpenseReviewStatus }\n> = {\n review: { from: ['unreviewed'], to: 'reviewed' },\n reject: { from: ['unreviewed', 'reviewed'], to: 'rejected' },\n markDuplicate: { from: ['unreviewed', 'reviewed'], to: 'rejected' },\n reopen: { from: ['reviewed', 'rejected'], to: 'unreviewed' },\n};\n\n/**\n * Fields a reviewed expense freezes. Changing one needs `reopen()` first, so\n * a reviewed amount can never move without a fresh review.\n */\nconst LOCKED_WHEN_REVIEWED = [\n ['amount', 'amount'],\n ['currency', 'currency'],\n ['incurredOn', 'incurred_on'],\n ['recordedAt', 'recorded_at'],\n ['category', 'category'],\n ['costObjectType', 'cost_object_type'],\n ['costObjectId', 'cost_object_id'],\n ['vendorId', 'vendor_id'],\n ['commitmentId', 'commitment_id'],\n ['commitmentLineId', 'commitment_line_id'],\n ['paidBy', 'paid_by'],\n ['paidByProfileId', 'paid_by_profile_id'],\n] as const;\n\n/** The review transition a dedicated method has authorized for one save. */\nconst authorizedTransition = new WeakMap<\n Expense,\n { from: ExpenseReviewStatus; expected: ReviewFieldState }\n>();\n\n/** Every review field, normalized for comparison. */\ninterface ReviewFieldState {\n status: ExpenseReviewStatus;\n reviewer: string | null;\n reviewedAtMs: number | null;\n note: string;\n duplicateOf: string | null;\n}\n\nfunction sameReviewState(a: ReviewFieldState, b: ReviewFieldState): boolean {\n return (\n a.status === b.status &&\n a.reviewer === b.reviewer &&\n a.reviewedAtMs === b.reviewedAtMs &&\n a.note === b.note &&\n a.duplicateOf === b.duplicateOf\n );\n}\n\ntype PersistedRow = Record<string, unknown>;\n\nfunction asNullableString(value: unknown): string | null {\n return value === null || value === undefined || value === ''\n ? null\n : String(value);\n}\n\n/**\n * A cost someone incurred.\n *\n * - **Cost object** — polymorphic `(costObjectType, costObjectId)`: a job, a\n * work package, a project, any registered SMRT object. Exactly one per\n * expense, so a cost is never rolled up into two jobs.\n * - **Money** — `amount` in integer minor units with an ISO 4217 `currency`.\n * - **Dates** — `incurredOn` (calendar date, `YYYY-MM-DD`) is when the cost\n * happened; `recordedAt` is when it was entered.\n * - **Review** — `unreviewed → reviewed | rejected`, only through\n * {@link Expense.review}, {@link Expense.reject},\n * {@link Expense.markDuplicate} and {@link Expense.reopen}. A plain field\n * write to the review state is refused on save.\n * - **Reimbursement** — recording only: whether a person who paid is owed\n * back and when they were paid. Never changes review state.\n * - **Commitment** — optional commerce `Contract` (usually a `PurchaseOrder`)\n * or one of its lines that this actual draws down.\n *\n * The generated REST/MCP/CLI surface is closed (`api: { include: [] }`):\n * cost visibility is permission-gated by every consumer, so they expose\n * expenses through their own routes and call the domain methods here.\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'expenses',\n api: { include: [] },\n mcp: { include: [] },\n cli: false,\n indexes: [\n {\n name: 'expenses_cost_object_idx',\n columns: ['tenantId', 'costObjectType', 'costObjectId', 'incurredOn'],\n },\n {\n name: 'expenses_commitment_review_idx',\n columns: ['tenantId', 'commitmentId', 'reviewStatus'],\n },\n {\n name: 'expenses_review_status_idx',\n columns: ['tenantId', 'reviewStatus', 'incurredOn'],\n },\n ],\n})\nexport class Expense extends SmrtObject {\n /** Owning tenant. Nullable: optional tenancy, like every commerce model. */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * Qualified class of the cost object, e.g.\n * `@happyvertical/smrt-projects:Project`. Empty only for an unallocated\n * expense (then `costObjectId` is empty too).\n */\n @field({ type: 'text' })\n costObjectType: string = '';\n\n /** Id of the cost object. Bare string: the target table is polymorphic. */\n @field({ type: 'text' })\n costObjectId: string = '';\n\n /**\n * Lowercase kebab-case category. See `SUGGESTED_EXPENSE_CATEGORIES`;\n * any value matching the pattern is accepted.\n */\n @field({ type: 'text' })\n category: string = 'other';\n\n /** What was bought or paid for. */\n @field({ type: 'text' })\n description: string = '';\n\n /** Amount in integer minor units (cents). `= 0` makes the column INTEGER. */\n @field({ type: 'integer' })\n amount: number = 0;\n\n /** ISO 4217 currency code, upper case. Required. */\n @field({ type: 'text' })\n currency: string = '';\n\n /** Calendar date the cost was incurred, `YYYY-MM-DD`. Required. */\n @field({ type: 'text' })\n incurredOn: string = '';\n\n /** When the expense was recorded; set on first save when empty. */\n recordedAt: Date | null = null;\n\n /** Profile that recorded the expense. */\n @crossPackageRef(PROFILE)\n recordedByProfileId: string | null = null;\n\n /** `company`, or `person` (then {@link Expense.paidByProfileId} is set). */\n @field({ type: 'text' })\n paidBy: ExpensePayer = 'company';\n\n /** Profile that paid out of pocket; required when `paidBy` is `person`. */\n @crossPackageRef(PROFILE)\n paidByProfileId: string | null = null;\n\n /** Optional commerce vendor the cost was paid to. */\n @crossPackageRef('@happyvertical/smrt-commerce:Vendor')\n vendorId: string | null = null;\n\n /**\n * Optional commitment this actual draws down: a commerce `Contract`\n * (normally a `PurchaseOrder`). Must be in the same currency and tenant.\n */\n @crossPackageRef('@happyvertical/smrt-commerce:Contract')\n commitmentId: string | null = null;\n\n /** Optional line of {@link Expense.commitmentId} this actual draws down. */\n @crossPackageRef('@happyvertical/smrt-commerce:ContractLineItem')\n commitmentLineId: string | null = null;\n\n /** Review state. Changed only by the review methods. */\n @field({ type: 'text', readonly: true })\n reviewStatus: ExpenseReviewStatus = 'unreviewed';\n\n /** Profile that made the current review decision. */\n @crossPackageRef(PROFILE, { readonly: true })\n reviewedByProfileId: string | null = null;\n\n /** When the current review decision was made. */\n @field({ type: 'datetime', readonly: true })\n reviewedAt: Date | null = null;\n\n /** Review note or rejection reason. */\n @field({ type: 'text', readonly: true })\n reviewNote: string = '';\n\n /**\n * Set when a reviewer marked this expense a duplicate of another one. A\n * duplicate is `rejected` and never counts toward any total.\n */\n @foreignKey('Expense', { readonly: true, onDelete: 'SET NULL' })\n duplicateOfId: string | null = null;\n\n /** Whether the person who paid is owed the amount back. */\n @field({ type: 'boolean' })\n reimbursable: boolean = false;\n\n /** When the person was paid back; set by {@link Expense.markReimbursed}. */\n reimbursedAt: Date | null = null;\n\n /** Payroll line, payables reference, or transfer id of the payback. */\n @field({ type: 'text' })\n reimbursementReference: string = '';\n\n constructor(options: ExpenseOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.costObjectType !== undefined)\n this.costObjectType = options.costObjectType;\n if (options.costObjectId !== undefined)\n this.costObjectId = options.costObjectId;\n if (options.category !== undefined) this.category = options.category;\n if (options.description !== undefined)\n this.description = options.description;\n if (options.amount !== undefined) this.amount = options.amount;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.incurredOn !== undefined) this.incurredOn = options.incurredOn;\n if (options.recordedAt !== undefined) this.recordedAt = options.recordedAt;\n if (options.recordedByProfileId !== undefined)\n this.recordedByProfileId = options.recordedByProfileId;\n if (options.paidBy !== undefined) this.paidBy = options.paidBy;\n if (options.paidByProfileId !== undefined)\n this.paidByProfileId = options.paidByProfileId;\n if (options.vendorId !== undefined) this.vendorId = options.vendorId;\n if (options.commitmentId !== undefined)\n this.commitmentId = options.commitmentId;\n if (options.commitmentLineId !== undefined)\n this.commitmentLineId = options.commitmentLineId;\n if (options.reimbursable !== undefined)\n this.reimbursable = options.reimbursable;\n if (options.reimbursedAt !== undefined)\n this.reimbursedAt = options.reimbursedAt;\n if (options.reimbursementReference !== undefined)\n this.reimbursementReference = options.reimbursementReference;\n }\n\n /** True when the person who paid has been paid back. */\n isReimbursed(): boolean {\n return this.reimbursedAt !== null && this.reimbursedAt !== undefined;\n }\n\n /** True when this expense counts toward reviewed totals and drawdown. */\n isCountable(): boolean {\n return this.reviewStatus === 'reviewed' && !this.duplicateOfId;\n }\n\n /**\n * Accept the expense as a real cost: `unreviewed → reviewed`.\n *\n * This is the separately authorizable review action: gate it in your route\n * or service (for example on `expenses.review`). The expense must already\n * be saved and must not be marked a duplicate.\n *\n * @throws {ExpenseError} `EXPENSE_REVIEW_TRANSITION` from any other state.\n */\n async review(input: ReviewExpenseInput): Promise<this> {\n const reviewer = requireReviewer(input?.reviewerProfileId);\n return this.transition('review', () => {\n this.reviewStatus = 'reviewed';\n this.reviewedByProfileId = reviewer;\n this.reviewedAt = input.at ?? new Date();\n this.reviewNote = input.note ?? '';\n this.duplicateOfId = null;\n });\n }\n\n /**\n * Refuse the expense: `unreviewed | reviewed → rejected`. A rejected\n * expense is kept and shown, and never counted.\n *\n * @throws {ExpenseError} `EXPENSE_REVIEW_TRANSITION` from `rejected`, or\n * `EXPENSE_INVALID` without a reason.\n */\n async reject(input: RejectExpenseInput): Promise<this> {\n const reviewer = requireReviewer(input?.reviewerProfileId);\n const reason = (input.reason ?? '').trim();\n if (!reason) {\n throw new ExpenseError('EXPENSE_INVALID', 'A rejection needs a reason.');\n }\n return this.transition('reject', () => {\n this.reviewStatus = 'rejected';\n this.reviewedByProfileId = reviewer;\n this.reviewedAt = input.at ?? new Date();\n this.reviewNote = reason;\n this.duplicateOfId = null;\n });\n }\n\n /**\n * Mark this expense a duplicate of another expense in the same tenant:\n * `unreviewed | reviewed → rejected` with {@link Expense.duplicateOfId}\n * set. A duplicate never counts toward any total.\n *\n * @throws {ExpenseError} `EXPENSE_INVALID` when the target is this expense\n * or is not visible in this expense's tenant.\n */\n async markDuplicate(input: MarkDuplicateExpenseInput): Promise<this> {\n const reviewer = requireReviewer(input?.reviewerProfileId);\n const targetId = String(input.duplicateOfId ?? '').trim();\n if (!targetId || targetId === this.id) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'An expense can only be a duplicate of a different expense.',\n );\n }\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const expenses = await ExpenseCollection.create({ db: this.db });\n const target = await expenses.get({ id: targetId });\n if (!target || (target.tenantId ?? null) !== (this.tenantId ?? null)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${targetId} was not found in this expense's tenant.`,\n );\n }\n return this.transition('markDuplicate', () => {\n this.reviewStatus = 'rejected';\n this.reviewedByProfileId = reviewer;\n this.reviewedAt = input.at ?? new Date();\n this.reviewNote = `Duplicate of expense ${targetId}`;\n this.duplicateOfId = targetId;\n });\n }\n\n /**\n * Send the expense back for review: `reviewed | rejected → unreviewed`.\n * Clears the reviewer, the review time and any duplicate marker.\n *\n * @throws {ExpenseError} `EXPENSE_REVIEW_TRANSITION` from `unreviewed`.\n */\n async reopen(input: ReopenExpenseInput): Promise<this> {\n requireReviewer(input?.reviewerProfileId);\n return this.transition('reopen', () => {\n this.reviewStatus = 'unreviewed';\n this.reviewedByProfileId = null;\n this.reviewedAt = null;\n this.reviewNote = input.note ?? '';\n this.duplicateOfId = null;\n });\n }\n\n /**\n * Record that the person who paid has been paid back. Recording only:\n * paying happens in payroll or payables, and this never changes the review\n * state.\n *\n * @throws {ExpenseError} `EXPENSE_INVALID` unless the expense is\n * `reimbursable`.\n */\n async markReimbursed(input: MarkReimbursedInput = {}): Promise<this> {\n if (!this.reimbursable) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${this.id} is not owed back to anyone.`,\n );\n }\n this.reimbursedAt = input.at ?? new Date();\n if (input.reference !== undefined)\n this.reimbursementReference = input.reference;\n return this.save();\n }\n\n /**\n * Load the cost object this expense is charged to, through its registered\n * collection (so tenant interceptors apply). Returns `null` when unset,\n * when the type is not registered, when the row is gone, or when the row\n * belongs to a different tenant than this expense.\n */\n async getCostObject<T extends SmrtObject = SmrtObject>(): Promise<T | null> {\n if (!this.costObjectType || !this.costObjectId) return null;\n let registered = ObjectRegistry.getClassByQualifiedName(\n this.costObjectType,\n );\n if (!registered) {\n const loaded = await ObjectRegistry.tryLoadFromExternalPackage(\n this.costObjectType,\n );\n if (!loaded) return null;\n registered = ObjectRegistry.getClassByQualifiedName(this.costObjectType);\n }\n if (!registered) return null;\n const collection = await ObjectRegistry.getCollection<SmrtObject>(\n registered.qualifiedName ?? this.costObjectType,\n { db: this.db },\n );\n const target = await collection.get({ id: this.costObjectId });\n if (!target) return null;\n const targetTenant = (target as unknown as { tenantId?: string | null })\n .tenantId;\n if (\n targetTenant !== undefined &&\n targetTenant !== null &&\n this.tenantId !== null &&\n targetTenant !== this.tenantId\n ) {\n return null;\n }\n return target as T;\n }\n\n /**\n * Validate, enforce the review and reviewed-field locks, check the\n * commitment, then persist.\n */\n override async save(): Promise<this> {\n this.normalize();\n this.validateShape();\n\n const persisted = await this.readPersisted();\n // The guards below check `persisted`; pin the write to that row.\n const inserting = await pinNaturalKey('Expense', this, persisted);\n if (\n persisted &&\n String(persisted.tenant_id ?? '') !==\n String(effectiveTenant(this.tenantId) ?? '')\n ) {\n // Includes the quiet case: saving a global expense inside a tenant\n // context, where the interceptor would fill in that tenant and split\n // the expense from its global receipts and links.\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${this.id}: its tenant cannot change after it is recorded.`,\n );\n }\n this.assertReviewFields(persisted);\n this.assertReviewedLock(persisted);\n await this.assertCommitment(persisted);\n await this.assertVendor(persisted);\n\n if (!this.recordedAt) this.recordedAt = new Date();\n try {\n return await super.save();\n } catch (error) {\n // A new expense is a plain INSERT; its only unique keys are the id and\n // the natural key, so a violation means it named an existing row.\n if (inserting && isUniqueViolationError(error)) {\n throw identityConflict('Expense', this, error);\n }\n throw error;\n }\n }\n\n private normalize(): void {\n this.tenantId = normalizeTenantId(this.tenantId);\n this.currency = String(this.currency ?? '')\n .trim()\n .toUpperCase();\n this.category = String(this.category ?? '').trim();\n this.costObjectType = String(this.costObjectType ?? '').trim();\n this.costObjectId = String(this.costObjectId ?? '').trim();\n this.incurredOn = String(this.incurredOn ?? '').trim();\n this.vendorId = asNullableString(this.vendorId);\n this.commitmentId = asNullableString(this.commitmentId);\n this.commitmentLineId = asNullableString(this.commitmentLineId);\n this.paidByProfileId = asNullableString(this.paidByProfileId);\n this.recordedByProfileId = asNullableString(this.recordedByProfileId);\n }\n\n private validateShape(): void {\n const label = this.id || '<new>';\n assertMinorUnits('Expense', label, 'amount', this.amount);\n assertIsoCurrency('Expense', label, this.currency);\n assertIsoDate('Expense', label, 'incurredOn', this.incurredOn);\n if (!CATEGORY_PATTERN.test(this.category) || this.category.length > 64) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: category must be lowercase kebab-case ` +\n `(for example 'outside-service'), got '${this.category}'.`,\n );\n }\n if (Boolean(this.costObjectType) !== Boolean(this.costObjectId)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: costObjectType and costObjectId are set together ` +\n 'or not at all.',\n );\n }\n if (this.costObjectType) {\n assertQualifiedClassName('Expense', label, this.costObjectType);\n }\n if (this.paidBy !== 'company' && this.paidBy !== 'person') {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: paidBy must be 'company' or 'person'.`,\n );\n }\n if (this.paidBy === 'person' && !this.paidByProfileId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: an expense paid by a person names that person ` +\n '(paidByProfileId).',\n );\n }\n if (this.paidBy === 'company' && this.paidByProfileId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: paidByProfileId is only set when paidBy is 'person'.`,\n );\n }\n if (this.reimbursable && this.paidBy !== 'person') {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: only an expense a person paid can be owed back.`,\n );\n }\n if (this.reimbursedAt && !this.reimbursable) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: reimbursedAt needs reimbursable.`,\n );\n }\n if (this.reviewStatus === 'reviewed' && this.duplicateOfId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: a reviewed expense cannot be marked a duplicate.`,\n );\n }\n if (this.commitmentLineId && !this.commitmentId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: a commitment line needs its commitment ` +\n '(commitmentId).',\n );\n }\n }\n\n /**\n * Read the stored row directly. The authoritative prior state for the\n * review and lock guards: the in-memory copy may have been mass-assigned.\n */\n private async readPersisted(): Promise<PersistedRow | null> {\n if (!this.id) return null;\n const row = await this.db.get(this.tableName, { id: this.id });\n return (row as PersistedRow | null | undefined) ?? null;\n }\n\n private reviewState(): ReviewFieldState {\n return {\n status: this.reviewStatus,\n reviewer: asNullableString(this.reviewedByProfileId),\n reviewedAtMs: instantMs(this.reviewedAt),\n note: this.reviewNote ?? '',\n duplicateOf: asNullableString(this.duplicateOfId),\n };\n }\n\n /**\n * Refuse any review-field change a transition did not authorize. The token\n * binds the transition's complete resulting state — status, reviewer,\n * time, note and duplicate marker — so a field set directly beside a\n * legitimate `review()` / `reject()` cannot ride along with it.\n */\n private assertReviewFields(persisted: PersistedRow | null): void {\n const token = authorizedTransition.get(this);\n const prior: ReviewFieldState = {\n status: (persisted?.review_status ?? 'unreviewed') as ExpenseReviewStatus,\n reviewer: asNullableString(persisted?.reviewed_by_profile_id),\n reviewedAtMs: instantMs(persisted?.reviewed_at),\n note: String(persisted?.review_note ?? ''),\n duplicateOf: asNullableString(persisted?.duplicate_of_id),\n };\n const current = this.reviewState();\n if (sameReviewState(current, prior)) return;\n if (\n !token ||\n token.from !== prior.status ||\n !sameReviewState(current, token.expected)\n ) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_FIELDS_LOCKED',\n `Expense ${this.id || '<new>'}: review state changes only through ` +\n 'review(), reject(), markDuplicate() or reopen().',\n );\n }\n }\n\n private assertReviewedLock(persisted: PersistedRow | null): void {\n if (persisted?.review_status !== 'reviewed') return;\n const self = this as unknown as Record<string, unknown>;\n for (const [fieldName, column] of LOCKED_WHEN_REVIEWED) {\n const before = persisted[column];\n const after = self[fieldName];\n const same =\n fieldName === 'amount'\n ? Number(before) === Number(after)\n : fieldName === 'recordedAt'\n ? instantMs(before) === instantMs(after)\n : asNullableString(before) === asNullableString(after);\n if (!same) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_FIELDS_LOCKED',\n `Expense ${this.id}: ${fieldName} of a reviewed expense cannot ` +\n 'change; reopen() it first.',\n );\n }\n }\n }\n\n /**\n * A matched commitment must be visible in this expense's tenant, owned by\n * the same tenant, and in the same currency; a matched line must belong to\n * it. Mixed currencies are refused here rather than summed later.\n *\n * Checked when the match is made or changed, not on every save: a later\n * change to the commitment (its currency, or its deletion) must not stop\n * an unrelated save such as recording a reimbursement.\n */\n private async assertCommitment(\n persisted: PersistedRow | null,\n ): Promise<void> {\n if (!this.commitmentId) return;\n if (\n persisted &&\n asNullableString(persisted.commitment_id) === this.commitmentId &&\n asNullableString(persisted.commitment_line_id) ===\n this.commitmentLineId &&\n String(persisted.currency ?? '') === this.currency\n ) {\n return;\n }\n const contracts = await ContractCollection.create({ db: this.db });\n const commitment = await contracts.get({ id: this.commitmentId });\n // Before super.save() the tenant interceptor has not populated tenantId\n // yet, so compare against the tenant the row is about to be written in.\n const owner = effectiveTenant(this.tenantId);\n if (!commitment || !sameTenant(commitment.tenantId, owner)) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Expense ${this.id || '<new>'}: commitment ${this.commitmentId} was ` +\n \"not found in this expense's tenant.\",\n );\n }\n if (String(commitment.currency).toUpperCase() !== this.currency) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Expense ${this.id || '<new>'}: currency ${this.currency} does not ` +\n `match commitment ${this.commitmentId} (${commitment.currency}). ` +\n 'Amounts in different currencies are never summed.',\n );\n }\n if (this.commitmentLineId) {\n const lines = await ContractLineItemCollection.create({ db: this.db });\n const line = await lines.get({ id: this.commitmentLineId });\n if (!line || line.contractId !== this.commitmentId) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Expense ${this.id || '<new>'}: line ${this.commitmentLineId} is ` +\n `not a line of commitment ${this.commitmentId}.`,\n );\n }\n }\n }\n\n /**\n * A vendor must be visible in, and owned by, the expense's tenant. A\n * `crossPackageRef` neither validates its target nor creates a foreign\n * key, so without this an expense in one tenant could name another\n * tenant's vendor. Checked when the vendor is set or changed.\n */\n private async assertVendor(persisted: PersistedRow | null): Promise<void> {\n if (!this.vendorId) return;\n if (persisted && asNullableString(persisted.vendor_id) === this.vendorId) {\n return;\n }\n const vendors = await VendorCollection.create({ db: this.db });\n const vendor = await vendors.get({ id: this.vendorId });\n const owner = effectiveTenant(this.tenantId);\n if (!vendor || !sameTenant(vendor.tenantId, owner)) {\n throw new ExpenseError(\n 'EXPENSE_VENDOR_MISMATCH',\n `Expense ${this.id || '<new>'}: vendor ${this.vendorId} was not ` +\n \"found in this expense's tenant.\",\n );\n }\n }\n\n private async transition(\n action: keyof typeof REVIEW_TRANSITIONS,\n apply: () => void,\n ): Promise<this> {\n const rule = REVIEW_TRANSITIONS[action];\n const persisted = await this.readPersisted();\n if (!persisted) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_TRANSITION',\n `Expense ${this.id || '<new>'}: save the expense before ${action}().`,\n );\n }\n const from = String(persisted.review_status) as ExpenseReviewStatus;\n if (!rule.from.includes(from)) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_TRANSITION',\n `Expense ${this.id}: cannot ${action}() an expense that is ${from}.`,\n );\n }\n if (action === 'review' && persisted.duplicate_of_id) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_TRANSITION',\n `Expense ${this.id}: a duplicate cannot be reviewed; reopen() it first.`,\n );\n }\n const snapshot = {\n reviewStatus: this.reviewStatus,\n reviewedByProfileId: this.reviewedByProfileId,\n reviewedAt: this.reviewedAt,\n reviewNote: this.reviewNote,\n duplicateOfId: this.duplicateOfId,\n };\n // Every transition assigns every review field, so the token below\n // describes the whole resulting review state.\n apply();\n authorizedTransition.set(this, { from, expected: this.reviewState() });\n try {\n return await this.save();\n } catch (error) {\n Object.assign(this, snapshot);\n throw error;\n } finally {\n authorizedTransition.delete(this);\n }\n }\n}\n\nfunction requireReviewer(reviewerProfileId: string | undefined): string {\n const reviewer = String(reviewerProfileId ?? '').trim();\n if (!reviewer) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'A review decision names its reviewer (reviewerProfileId).',\n );\n }\n return reviewer;\n}\n\nfunction sameTenant(\n a: string | null | undefined,\n b: string | null | undefined,\n): boolean {\n return (a ?? null) === (b ?? null);\n}\n"],"mappings":"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"}
|