@voyant-travel/finance 0.173.0 → 0.175.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/reporting-definitions.d.ts +310 -0
- package/dist/reporting-definitions.js +271 -0
- package/dist/reporting.d.ts +17 -0
- package/dist/reporting.js +363 -0
- package/dist/voyant.js +68 -0
- package/package.json +17 -11
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
export declare const FINANCE_RECEIVABLES_DATASET_ID = "finance.receivables";
|
|
2
|
+
/**
|
|
3
|
+
* Finance-owned semantics for issued customer receivables.
|
|
4
|
+
*
|
|
5
|
+
* Amounts remain in the document currency. Consumers must group by currency
|
|
6
|
+
* or constrain a query to one currency; the executor enforces that invariant.
|
|
7
|
+
*/
|
|
8
|
+
export declare const financeReceivablesDatasetDefinition: {
|
|
9
|
+
id: string;
|
|
10
|
+
version: number;
|
|
11
|
+
label: string;
|
|
12
|
+
description: string;
|
|
13
|
+
grain: string;
|
|
14
|
+
requiredScopes: string[];
|
|
15
|
+
defaultLimit: number;
|
|
16
|
+
maximumLimit: number;
|
|
17
|
+
fields: ({
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
description: string;
|
|
21
|
+
role: "dimension";
|
|
22
|
+
valueType: "date";
|
|
23
|
+
sensitivity: "internal";
|
|
24
|
+
requiredScopes: string[];
|
|
25
|
+
aggregations: never[];
|
|
26
|
+
} | {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
role: "dimension";
|
|
30
|
+
valueType: "date";
|
|
31
|
+
sensitivity: "internal";
|
|
32
|
+
requiredScopes: string[];
|
|
33
|
+
aggregations: never[];
|
|
34
|
+
description?: undefined;
|
|
35
|
+
} | {
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
description: string;
|
|
39
|
+
role: "dimension";
|
|
40
|
+
valueType: "string";
|
|
41
|
+
sensitivity: "internal";
|
|
42
|
+
requiredScopes: string[];
|
|
43
|
+
aggregations: ("count" | "countDistinct")[];
|
|
44
|
+
} | {
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
description: string;
|
|
48
|
+
role: "measure";
|
|
49
|
+
valueType: "currency";
|
|
50
|
+
sensitivity: "sensitive";
|
|
51
|
+
requiredScopes: string[];
|
|
52
|
+
aggregations: "sum"[];
|
|
53
|
+
})[];
|
|
54
|
+
};
|
|
55
|
+
export declare const financeReportingWidgets: ({
|
|
56
|
+
id: string;
|
|
57
|
+
version: number;
|
|
58
|
+
label: string;
|
|
59
|
+
description: string;
|
|
60
|
+
query: {
|
|
61
|
+
dataset: {
|
|
62
|
+
id: string;
|
|
63
|
+
version: number;
|
|
64
|
+
};
|
|
65
|
+
select: ({
|
|
66
|
+
kind: "field";
|
|
67
|
+
field: string;
|
|
68
|
+
operation?: undefined;
|
|
69
|
+
as?: undefined;
|
|
70
|
+
} | {
|
|
71
|
+
kind: "aggregate";
|
|
72
|
+
operation: "sum";
|
|
73
|
+
field: string;
|
|
74
|
+
as: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: "aggregate";
|
|
77
|
+
operation: "count";
|
|
78
|
+
as: string;
|
|
79
|
+
field?: undefined;
|
|
80
|
+
})[];
|
|
81
|
+
filters: never[];
|
|
82
|
+
groupBy: {
|
|
83
|
+
field: string;
|
|
84
|
+
}[];
|
|
85
|
+
orderBy: {
|
|
86
|
+
by: string;
|
|
87
|
+
direction: "descending";
|
|
88
|
+
}[];
|
|
89
|
+
limit: number;
|
|
90
|
+
};
|
|
91
|
+
visualization: {
|
|
92
|
+
type: "bar";
|
|
93
|
+
options: {
|
|
94
|
+
category: string;
|
|
95
|
+
value: string;
|
|
96
|
+
currencyField: string;
|
|
97
|
+
minorUnit: boolean;
|
|
98
|
+
x?: undefined;
|
|
99
|
+
y?: undefined;
|
|
100
|
+
series?: undefined;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
defaultSize: {
|
|
104
|
+
width: number;
|
|
105
|
+
height: number;
|
|
106
|
+
};
|
|
107
|
+
minimumSize: {
|
|
108
|
+
width: number;
|
|
109
|
+
height: number;
|
|
110
|
+
};
|
|
111
|
+
maximumSize: {
|
|
112
|
+
width: number;
|
|
113
|
+
height: number;
|
|
114
|
+
};
|
|
115
|
+
} | {
|
|
116
|
+
id: string;
|
|
117
|
+
version: number;
|
|
118
|
+
label: string;
|
|
119
|
+
description: string;
|
|
120
|
+
query: {
|
|
121
|
+
dataset: {
|
|
122
|
+
id: string;
|
|
123
|
+
version: number;
|
|
124
|
+
};
|
|
125
|
+
select: ({
|
|
126
|
+
kind: "field";
|
|
127
|
+
field: string;
|
|
128
|
+
as: string;
|
|
129
|
+
operation?: undefined;
|
|
130
|
+
} | {
|
|
131
|
+
kind: "field";
|
|
132
|
+
field: string;
|
|
133
|
+
as?: undefined;
|
|
134
|
+
operation?: undefined;
|
|
135
|
+
} | {
|
|
136
|
+
kind: "aggregate";
|
|
137
|
+
operation: "sum";
|
|
138
|
+
field: string;
|
|
139
|
+
as: string;
|
|
140
|
+
})[];
|
|
141
|
+
filters: never[];
|
|
142
|
+
groupBy: ({
|
|
143
|
+
field: string;
|
|
144
|
+
timeGrain: "month";
|
|
145
|
+
} | {
|
|
146
|
+
field: string;
|
|
147
|
+
timeGrain?: undefined;
|
|
148
|
+
})[];
|
|
149
|
+
orderBy: {
|
|
150
|
+
by: string;
|
|
151
|
+
direction: "ascending";
|
|
152
|
+
}[];
|
|
153
|
+
limit: number;
|
|
154
|
+
};
|
|
155
|
+
visualization: {
|
|
156
|
+
type: "line";
|
|
157
|
+
options: {
|
|
158
|
+
x: string;
|
|
159
|
+
y: string;
|
|
160
|
+
series: string;
|
|
161
|
+
currencyField: string;
|
|
162
|
+
minorUnit: boolean;
|
|
163
|
+
category?: undefined;
|
|
164
|
+
value?: undefined;
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
defaultSize: {
|
|
168
|
+
width: number;
|
|
169
|
+
height: number;
|
|
170
|
+
};
|
|
171
|
+
minimumSize: {
|
|
172
|
+
width: number;
|
|
173
|
+
height: number;
|
|
174
|
+
};
|
|
175
|
+
maximumSize: {
|
|
176
|
+
width: number;
|
|
177
|
+
height: number;
|
|
178
|
+
};
|
|
179
|
+
} | {
|
|
180
|
+
id: string;
|
|
181
|
+
version: number;
|
|
182
|
+
label: string;
|
|
183
|
+
description: string;
|
|
184
|
+
query: {
|
|
185
|
+
dataset: {
|
|
186
|
+
id: string;
|
|
187
|
+
version: number;
|
|
188
|
+
};
|
|
189
|
+
select: ({
|
|
190
|
+
kind: "field";
|
|
191
|
+
field: string;
|
|
192
|
+
operation?: undefined;
|
|
193
|
+
as?: undefined;
|
|
194
|
+
} | {
|
|
195
|
+
kind: "aggregate";
|
|
196
|
+
operation: "count";
|
|
197
|
+
as: string;
|
|
198
|
+
field?: undefined;
|
|
199
|
+
})[];
|
|
200
|
+
filters: never[];
|
|
201
|
+
groupBy: {
|
|
202
|
+
field: string;
|
|
203
|
+
}[];
|
|
204
|
+
orderBy: {
|
|
205
|
+
by: string;
|
|
206
|
+
direction: "descending";
|
|
207
|
+
}[];
|
|
208
|
+
limit: number;
|
|
209
|
+
};
|
|
210
|
+
visualization: {
|
|
211
|
+
type: "pie";
|
|
212
|
+
options: {
|
|
213
|
+
category: string;
|
|
214
|
+
value: string;
|
|
215
|
+
currencyField?: undefined;
|
|
216
|
+
minorUnit?: undefined;
|
|
217
|
+
x?: undefined;
|
|
218
|
+
y?: undefined;
|
|
219
|
+
series?: undefined;
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
defaultSize: {
|
|
223
|
+
width: number;
|
|
224
|
+
height: number;
|
|
225
|
+
};
|
|
226
|
+
minimumSize: {
|
|
227
|
+
width: number;
|
|
228
|
+
height: number;
|
|
229
|
+
};
|
|
230
|
+
maximumSize: {
|
|
231
|
+
width: number;
|
|
232
|
+
height: number;
|
|
233
|
+
};
|
|
234
|
+
} | {
|
|
235
|
+
id: string;
|
|
236
|
+
version: number;
|
|
237
|
+
label: string;
|
|
238
|
+
description: string;
|
|
239
|
+
query: {
|
|
240
|
+
dataset: {
|
|
241
|
+
id: string;
|
|
242
|
+
version: number;
|
|
243
|
+
};
|
|
244
|
+
select: ({
|
|
245
|
+
kind: "field";
|
|
246
|
+
field: string;
|
|
247
|
+
operation?: undefined;
|
|
248
|
+
as?: undefined;
|
|
249
|
+
} | {
|
|
250
|
+
kind: "aggregate";
|
|
251
|
+
operation: "sum";
|
|
252
|
+
field: string;
|
|
253
|
+
as: string;
|
|
254
|
+
})[];
|
|
255
|
+
filters: never[];
|
|
256
|
+
groupBy: {
|
|
257
|
+
field: string;
|
|
258
|
+
}[];
|
|
259
|
+
orderBy: {
|
|
260
|
+
by: string;
|
|
261
|
+
direction: "descending";
|
|
262
|
+
}[];
|
|
263
|
+
limit: number;
|
|
264
|
+
};
|
|
265
|
+
visualization: {
|
|
266
|
+
type: "table";
|
|
267
|
+
options: {
|
|
268
|
+
currencyField: string;
|
|
269
|
+
minorUnit: boolean;
|
|
270
|
+
category?: undefined;
|
|
271
|
+
value?: undefined;
|
|
272
|
+
x?: undefined;
|
|
273
|
+
y?: undefined;
|
|
274
|
+
series?: undefined;
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
defaultSize: {
|
|
278
|
+
width: number;
|
|
279
|
+
height: number;
|
|
280
|
+
};
|
|
281
|
+
minimumSize: {
|
|
282
|
+
width: number;
|
|
283
|
+
height: number;
|
|
284
|
+
};
|
|
285
|
+
maximumSize: {
|
|
286
|
+
width: number;
|
|
287
|
+
height: number;
|
|
288
|
+
};
|
|
289
|
+
})[];
|
|
290
|
+
export declare const financeReportingTemplates: {
|
|
291
|
+
id: string;
|
|
292
|
+
version: number;
|
|
293
|
+
label: string;
|
|
294
|
+
description: string;
|
|
295
|
+
parameters: never[];
|
|
296
|
+
widgets: {
|
|
297
|
+
id: string;
|
|
298
|
+
source: {
|
|
299
|
+
kind: "preset";
|
|
300
|
+
widgetId: string;
|
|
301
|
+
version: number;
|
|
302
|
+
};
|
|
303
|
+
layout: {
|
|
304
|
+
x: number;
|
|
305
|
+
y: number;
|
|
306
|
+
width: number;
|
|
307
|
+
height: number;
|
|
308
|
+
};
|
|
309
|
+
}[];
|
|
310
|
+
}[];
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
export const FINANCE_RECEIVABLES_DATASET_ID = "finance.receivables";
|
|
2
|
+
/**
|
|
3
|
+
* Finance-owned semantics for issued customer receivables.
|
|
4
|
+
*
|
|
5
|
+
* Amounts remain in the document currency. Consumers must group by currency
|
|
6
|
+
* or constrain a query to one currency; the executor enforces that invariant.
|
|
7
|
+
*/
|
|
8
|
+
export const financeReceivablesDatasetDefinition = {
|
|
9
|
+
id: FINANCE_RECEIVABLES_DATASET_ID,
|
|
10
|
+
version: 1,
|
|
11
|
+
label: "Receivables",
|
|
12
|
+
description: "Final, non-void customer invoices with issued credit notes and completed or refunded collections attributed in the invoice currency. Proformas and draft documents are excluded.",
|
|
13
|
+
grain: "One issued, partially paid, paid, or overdue final customer invoice.",
|
|
14
|
+
requiredScopes: ["finance:read"],
|
|
15
|
+
defaultLimit: 100,
|
|
16
|
+
maximumLimit: 1_000,
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
id: "issueDate",
|
|
20
|
+
label: "Issue date",
|
|
21
|
+
description: "The final invoice issue date; trends bucket this date in UTC calendar units.",
|
|
22
|
+
role: "dimension",
|
|
23
|
+
valueType: "date",
|
|
24
|
+
sensitivity: "internal",
|
|
25
|
+
requiredScopes: ["finance:read"],
|
|
26
|
+
aggregations: [],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "dueDate",
|
|
30
|
+
label: "Due date",
|
|
31
|
+
role: "dimension",
|
|
32
|
+
valueType: "date",
|
|
33
|
+
sensitivity: "internal",
|
|
34
|
+
requiredScopes: ["finance:read"],
|
|
35
|
+
aggregations: [],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "status",
|
|
39
|
+
label: "Invoice status",
|
|
40
|
+
description: "Current status of the final invoice; draft, pending allocation, and void documents are outside this dataset.",
|
|
41
|
+
role: "dimension",
|
|
42
|
+
valueType: "string",
|
|
43
|
+
sensitivity: "internal",
|
|
44
|
+
requiredScopes: ["finance:read"],
|
|
45
|
+
aggregations: ["count", "countDistinct"],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "currency",
|
|
49
|
+
label: "Document currency",
|
|
50
|
+
description: "ISO currency of the final invoice. No report-time FX conversion is applied.",
|
|
51
|
+
role: "dimension",
|
|
52
|
+
valueType: "string",
|
|
53
|
+
sensitivity: "internal",
|
|
54
|
+
requiredScopes: ["finance:read"],
|
|
55
|
+
aggregations: ["count", "countDistinct"],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "grossIssuedCents",
|
|
59
|
+
label: "Gross issued",
|
|
60
|
+
description: "Final invoice total before issued or applied credit notes, in document-currency minor units.",
|
|
61
|
+
role: "measure",
|
|
62
|
+
valueType: "currency",
|
|
63
|
+
sensitivity: "sensitive",
|
|
64
|
+
requiredScopes: ["finance:read"],
|
|
65
|
+
aggregations: ["sum"],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "creditedCents",
|
|
69
|
+
label: "Credited",
|
|
70
|
+
description: "Issued or applied credit notes converted with their persisted invoice-currency snapshot, in minor units.",
|
|
71
|
+
role: "measure",
|
|
72
|
+
valueType: "currency",
|
|
73
|
+
sensitivity: "sensitive",
|
|
74
|
+
requiredScopes: ["finance:read"],
|
|
75
|
+
aggregations: ["sum"],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "netIssuedCents",
|
|
79
|
+
label: "Net issued",
|
|
80
|
+
description: "Gross final invoice value less issued or applied credit notes, attributed to the final invoice issue date.",
|
|
81
|
+
role: "measure",
|
|
82
|
+
valueType: "currency",
|
|
83
|
+
sensitivity: "sensitive",
|
|
84
|
+
requiredScopes: ["finance:read"],
|
|
85
|
+
aggregations: ["sum"],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "settledCents",
|
|
89
|
+
label: "Settled",
|
|
90
|
+
description: "Payments whose current status is completed, converted with their persisted invoice-currency snapshot.",
|
|
91
|
+
role: "measure",
|
|
92
|
+
valueType: "currency",
|
|
93
|
+
sensitivity: "sensitive",
|
|
94
|
+
requiredScopes: ["finance:read"],
|
|
95
|
+
aggregations: ["sum"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "refundedCents",
|
|
99
|
+
label: "Refunded",
|
|
100
|
+
description: "Payments whose current status is refunded, converted with their persisted invoice-currency snapshot.",
|
|
101
|
+
role: "measure",
|
|
102
|
+
valueType: "currency",
|
|
103
|
+
sensitivity: "sensitive",
|
|
104
|
+
requiredScopes: ["finance:read"],
|
|
105
|
+
aggregations: ["sum"],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "outstandingBalanceCents",
|
|
109
|
+
label: "Outstanding balance",
|
|
110
|
+
description: "Net issued value less currently completed payments, floored at zero, in document-currency minor units.",
|
|
111
|
+
role: "measure",
|
|
112
|
+
valueType: "currency",
|
|
113
|
+
sensitivity: "sensitive",
|
|
114
|
+
requiredScopes: ["finance:read"],
|
|
115
|
+
aggregations: ["sum"],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
export const financeReportingWidgets = [
|
|
120
|
+
{
|
|
121
|
+
id: "finance.outstanding-by-currency",
|
|
122
|
+
version: 1,
|
|
123
|
+
label: "Outstanding by currency",
|
|
124
|
+
description: "Current unpaid final-invoice balance, kept separate by document currency.",
|
|
125
|
+
query: {
|
|
126
|
+
dataset: { id: FINANCE_RECEIVABLES_DATASET_ID, version: 1 },
|
|
127
|
+
select: [
|
|
128
|
+
{ kind: "field", field: "currency" },
|
|
129
|
+
{
|
|
130
|
+
kind: "aggregate",
|
|
131
|
+
operation: "sum",
|
|
132
|
+
field: "outstandingBalanceCents",
|
|
133
|
+
as: "outstandingBalanceCents",
|
|
134
|
+
},
|
|
135
|
+
{ kind: "aggregate", operation: "count", as: "invoiceCount" },
|
|
136
|
+
],
|
|
137
|
+
filters: [],
|
|
138
|
+
groupBy: [{ field: "currency" }],
|
|
139
|
+
orderBy: [{ by: "outstandingBalanceCents", direction: "descending" }],
|
|
140
|
+
limit: 20,
|
|
141
|
+
},
|
|
142
|
+
visualization: {
|
|
143
|
+
type: "bar",
|
|
144
|
+
options: {
|
|
145
|
+
category: "currency",
|
|
146
|
+
value: "outstandingBalanceCents",
|
|
147
|
+
currencyField: "currency",
|
|
148
|
+
minorUnit: true,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
defaultSize: { width: 4, height: 3 },
|
|
152
|
+
minimumSize: { width: 3, height: 2 },
|
|
153
|
+
maximumSize: { width: 12, height: 8 },
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "finance.net-issued-trend",
|
|
157
|
+
version: 1,
|
|
158
|
+
label: "Net issued trend",
|
|
159
|
+
description: "Final invoices less issued/applied credit notes, attributed to the invoice issue month and separated by currency.",
|
|
160
|
+
query: {
|
|
161
|
+
dataset: { id: FINANCE_RECEIVABLES_DATASET_ID, version: 1 },
|
|
162
|
+
select: [
|
|
163
|
+
{ kind: "field", field: "issueDate", as: "issueMonth" },
|
|
164
|
+
{ kind: "field", field: "currency" },
|
|
165
|
+
{
|
|
166
|
+
kind: "aggregate",
|
|
167
|
+
operation: "sum",
|
|
168
|
+
field: "netIssuedCents",
|
|
169
|
+
as: "netIssuedCents",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
filters: [],
|
|
173
|
+
groupBy: [{ field: "issueDate", timeGrain: "month" }, { field: "currency" }],
|
|
174
|
+
orderBy: [
|
|
175
|
+
{ by: "issueMonth", direction: "ascending" },
|
|
176
|
+
{ by: "currency", direction: "ascending" },
|
|
177
|
+
],
|
|
178
|
+
limit: 240,
|
|
179
|
+
},
|
|
180
|
+
visualization: {
|
|
181
|
+
type: "line",
|
|
182
|
+
options: {
|
|
183
|
+
x: "issueMonth",
|
|
184
|
+
y: "netIssuedCents",
|
|
185
|
+
series: "currency",
|
|
186
|
+
currencyField: "currency",
|
|
187
|
+
minorUnit: true,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
defaultSize: { width: 8, height: 3 },
|
|
191
|
+
minimumSize: { width: 4, height: 3 },
|
|
192
|
+
maximumSize: { width: 12, height: 8 },
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "finance.invoice-status-breakdown",
|
|
196
|
+
version: 1,
|
|
197
|
+
label: "Final invoice status",
|
|
198
|
+
description: "Count of recognized final invoices by their current receivable status.",
|
|
199
|
+
query: {
|
|
200
|
+
dataset: { id: FINANCE_RECEIVABLES_DATASET_ID, version: 1 },
|
|
201
|
+
select: [
|
|
202
|
+
{ kind: "field", field: "status" },
|
|
203
|
+
{ kind: "aggregate", operation: "count", as: "invoiceCount" },
|
|
204
|
+
],
|
|
205
|
+
filters: [],
|
|
206
|
+
groupBy: [{ field: "status" }],
|
|
207
|
+
orderBy: [{ by: "invoiceCount", direction: "descending" }],
|
|
208
|
+
limit: 10,
|
|
209
|
+
},
|
|
210
|
+
visualization: { type: "pie", options: { category: "status", value: "invoiceCount" } },
|
|
211
|
+
defaultSize: { width: 4, height: 3 },
|
|
212
|
+
minimumSize: { width: 3, height: 3 },
|
|
213
|
+
maximumSize: { width: 8, height: 8 },
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "finance.collections-by-currency",
|
|
217
|
+
version: 1,
|
|
218
|
+
label: "Collections by currency",
|
|
219
|
+
description: "Currently completed and refunded customer payments attributed to final invoices.",
|
|
220
|
+
query: {
|
|
221
|
+
dataset: { id: FINANCE_RECEIVABLES_DATASET_ID, version: 1 },
|
|
222
|
+
select: [
|
|
223
|
+
{ kind: "field", field: "currency" },
|
|
224
|
+
{ kind: "aggregate", operation: "sum", field: "settledCents", as: "settledCents" },
|
|
225
|
+
{ kind: "aggregate", operation: "sum", field: "refundedCents", as: "refundedCents" },
|
|
226
|
+
],
|
|
227
|
+
filters: [],
|
|
228
|
+
groupBy: [{ field: "currency" }],
|
|
229
|
+
orderBy: [{ by: "settledCents", direction: "descending" }],
|
|
230
|
+
limit: 20,
|
|
231
|
+
},
|
|
232
|
+
visualization: {
|
|
233
|
+
type: "table",
|
|
234
|
+
options: { currencyField: "currency", minorUnit: true },
|
|
235
|
+
},
|
|
236
|
+
defaultSize: { width: 4, height: 3 },
|
|
237
|
+
minimumSize: { width: 4, height: 2 },
|
|
238
|
+
maximumSize: { width: 12, height: 8 },
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
export const financeReportingTemplates = [
|
|
242
|
+
{
|
|
243
|
+
id: "finance.overview",
|
|
244
|
+
version: 1,
|
|
245
|
+
label: "Finance overview",
|
|
246
|
+
description: "Issued value, collections, outstanding receivables, and current final-invoice status.",
|
|
247
|
+
parameters: [],
|
|
248
|
+
widgets: [
|
|
249
|
+
{
|
|
250
|
+
id: "outstanding",
|
|
251
|
+
source: { kind: "preset", widgetId: "finance.outstanding-by-currency", version: 1 },
|
|
252
|
+
layout: { x: 0, y: 0, width: 4, height: 3 },
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: "net-issued",
|
|
256
|
+
source: { kind: "preset", widgetId: "finance.net-issued-trend", version: 1 },
|
|
257
|
+
layout: { x: 4, y: 0, width: 8, height: 3 },
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: "status",
|
|
261
|
+
source: { kind: "preset", widgetId: "finance.invoice-status-breakdown", version: 1 },
|
|
262
|
+
layout: { x: 0, y: 3, width: 4, height: 3 },
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: "collections",
|
|
266
|
+
source: { kind: "preset", widgetId: "finance.collections-by-currency", version: 1 },
|
|
267
|
+
layout: { x: 4, y: 3, width: 8, height: 3 },
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReportDatasetContribution, ReportDatasetExecutionInput, ReportResult } from "@voyant-travel/reporting-contracts";
|
|
2
|
+
import { type SQL } from "drizzle-orm";
|
|
3
|
+
/** A query shape outside Finance's deliberately small reporting surface. */
|
|
4
|
+
export declare class FinanceReportingQueryError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Compile the public single-dataset AST to parameter-bound SQL over the
|
|
9
|
+
* Finance-owned semantic receivables relation. Only the small set implemented
|
|
10
|
+
* here is accepted; query text can never address tables or columns directly.
|
|
11
|
+
*/
|
|
12
|
+
export declare function compileFinanceReceivablesQuery(input: ReportDatasetExecutionInput): {
|
|
13
|
+
statement: SQL;
|
|
14
|
+
columns: ReportResult["columns"];
|
|
15
|
+
rowLimit: number;
|
|
16
|
+
};
|
|
17
|
+
export declare const financeReceivablesDataset: ReportDatasetContribution;
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { hasApiKeyPermission, permissionStringsToPermissions } from "@voyant-travel/types/api-keys";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
import { FINANCE_RECEIVABLES_DATASET_ID, financeReceivablesDatasetDefinition, } from "./reporting-definitions.js";
|
|
4
|
+
import { executeBoundaryRows } from "./service-boundary-sql.js";
|
|
5
|
+
const MONEY_FIELDS = new Set([
|
|
6
|
+
"grossIssuedCents",
|
|
7
|
+
"creditedCents",
|
|
8
|
+
"netIssuedCents",
|
|
9
|
+
"settledCents",
|
|
10
|
+
"refundedCents",
|
|
11
|
+
"outstandingBalanceCents",
|
|
12
|
+
]);
|
|
13
|
+
const fieldSql = {
|
|
14
|
+
issueDate: sql `receivable."issueDate"`,
|
|
15
|
+
dueDate: sql `receivable."dueDate"`,
|
|
16
|
+
status: sql `receivable.status`,
|
|
17
|
+
currency: sql `receivable.currency`,
|
|
18
|
+
grossIssuedCents: sql `receivable."grossIssuedCents"`,
|
|
19
|
+
creditedCents: sql `receivable."creditedCents"`,
|
|
20
|
+
netIssuedCents: sql `receivable."netIssuedCents"`,
|
|
21
|
+
settledCents: sql `receivable."settledCents"`,
|
|
22
|
+
refundedCents: sql `receivable."refundedCents"`,
|
|
23
|
+
outstandingBalanceCents: sql `receivable."outstandingBalanceCents"`,
|
|
24
|
+
};
|
|
25
|
+
const semanticReceivables = sql `
|
|
26
|
+
SELECT
|
|
27
|
+
invoice.issue_date AS "issueDate",
|
|
28
|
+
invoice.due_date AS "dueDate",
|
|
29
|
+
invoice.status::text AS status,
|
|
30
|
+
invoice.currency,
|
|
31
|
+
invoice.total_cents::bigint AS "grossIssuedCents",
|
|
32
|
+
credit_totals.credited_cents::bigint AS "creditedCents",
|
|
33
|
+
greatest(invoice.total_cents::bigint - credit_totals.credited_cents, 0) AS "netIssuedCents",
|
|
34
|
+
payment_totals.settled_cents::bigint AS "settledCents",
|
|
35
|
+
payment_totals.refunded_cents::bigint AS "refundedCents",
|
|
36
|
+
greatest(
|
|
37
|
+
invoice.total_cents::bigint
|
|
38
|
+
- credit_totals.credited_cents
|
|
39
|
+
- payment_totals.settled_cents,
|
|
40
|
+
0
|
|
41
|
+
) AS "outstandingBalanceCents"
|
|
42
|
+
FROM invoices invoice
|
|
43
|
+
LEFT JOIN LATERAL (
|
|
44
|
+
SELECT coalesce(sum(
|
|
45
|
+
CASE
|
|
46
|
+
WHEN credit.currency = invoice.currency THEN credit.amount_cents
|
|
47
|
+
WHEN credit.base_currency = invoice.currency THEN coalesce(credit.base_amount_cents, 0)
|
|
48
|
+
ELSE 0
|
|
49
|
+
END
|
|
50
|
+
), 0)::bigint AS credited_cents
|
|
51
|
+
FROM credit_notes credit
|
|
52
|
+
WHERE credit.invoice_id = invoice.id
|
|
53
|
+
AND credit.status IN ('issued', 'applied')
|
|
54
|
+
) credit_totals ON true
|
|
55
|
+
LEFT JOIN LATERAL (
|
|
56
|
+
SELECT
|
|
57
|
+
coalesce(sum(CASE WHEN payment.status = 'completed' THEN
|
|
58
|
+
CASE
|
|
59
|
+
WHEN payment.currency = invoice.currency THEN payment.amount_cents
|
|
60
|
+
WHEN payment.base_currency = invoice.currency THEN coalesce(payment.base_amount_cents, 0)
|
|
61
|
+
ELSE 0
|
|
62
|
+
END
|
|
63
|
+
ELSE 0 END), 0)::bigint AS settled_cents,
|
|
64
|
+
coalesce(sum(CASE WHEN payment.status = 'refunded' THEN
|
|
65
|
+
CASE
|
|
66
|
+
WHEN payment.currency = invoice.currency THEN payment.amount_cents
|
|
67
|
+
WHEN payment.base_currency = invoice.currency THEN coalesce(payment.base_amount_cents, 0)
|
|
68
|
+
ELSE 0
|
|
69
|
+
END
|
|
70
|
+
ELSE 0 END), 0)::bigint AS refunded_cents
|
|
71
|
+
FROM payments payment
|
|
72
|
+
WHERE payment.invoice_id = invoice.id
|
|
73
|
+
) payment_totals ON true
|
|
74
|
+
WHERE invoice.invoice_type = 'invoice'
|
|
75
|
+
AND invoice.status IN ('issued', 'partially_paid', 'paid', 'overdue')
|
|
76
|
+
`;
|
|
77
|
+
/** A query shape outside Finance's deliberately small reporting surface. */
|
|
78
|
+
export class FinanceReportingQueryError extends Error {
|
|
79
|
+
constructor(message) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = "FinanceReportingQueryError";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compile the public single-dataset AST to parameter-bound SQL over the
|
|
86
|
+
* Finance-owned semantic receivables relation. Only the small set implemented
|
|
87
|
+
* here is accepted; query text can never address tables or columns directly.
|
|
88
|
+
*/
|
|
89
|
+
export function compileFinanceReceivablesQuery(input) {
|
|
90
|
+
const { query, parameters } = input;
|
|
91
|
+
if (!Number.isInteger(input.maximumRows) || input.maximumRows < 1) {
|
|
92
|
+
throw new FinanceReportingQueryError("maximumRows must be a positive integer.");
|
|
93
|
+
}
|
|
94
|
+
if (query.dataset.id !== FINANCE_RECEIVABLES_DATASET_ID) {
|
|
95
|
+
throw new FinanceReportingQueryError(`Unsupported dataset ${JSON.stringify(query.dataset.id)}.`);
|
|
96
|
+
}
|
|
97
|
+
if (query.dataset.version !== undefined && query.dataset.version !== 1) {
|
|
98
|
+
throw new FinanceReportingQueryError(`Unsupported receivables dataset version ${query.dataset.version}.`);
|
|
99
|
+
}
|
|
100
|
+
const definitions = new Map(financeReceivablesDatasetDefinition.fields.map((field) => [field.id, field]));
|
|
101
|
+
const groups = new Map();
|
|
102
|
+
for (const group of query.groupBy) {
|
|
103
|
+
const field = requireField(definitions, group.field);
|
|
104
|
+
if (field.role !== "dimension") {
|
|
105
|
+
throw new FinanceReportingQueryError(`Cannot group by measure ${JSON.stringify(group.field)}.`);
|
|
106
|
+
}
|
|
107
|
+
if (groups.has(group.field)) {
|
|
108
|
+
throw new FinanceReportingQueryError(`Duplicate group ${JSON.stringify(group.field)}.`);
|
|
109
|
+
}
|
|
110
|
+
if (group.timeGrain && field.valueType !== "date" && field.valueType !== "datetime") {
|
|
111
|
+
throw new FinanceReportingQueryError(`Time grain is not valid for ${JSON.stringify(group.field)}.`);
|
|
112
|
+
}
|
|
113
|
+
groups.set(group.field, group);
|
|
114
|
+
}
|
|
115
|
+
const aggregateQuery = query.select.some((selection) => selection.kind === "aggregate");
|
|
116
|
+
const selections = query.select.map((selection) => {
|
|
117
|
+
if (selection.kind === "field") {
|
|
118
|
+
const field = requireField(definitions, selection.field);
|
|
119
|
+
if (aggregateQuery && !groups.has(field.id)) {
|
|
120
|
+
throw new FinanceReportingQueryError(`Selected field ${JSON.stringify(field.id)} must appear in groupBy.`);
|
|
121
|
+
}
|
|
122
|
+
const id = selection.as ?? field.id;
|
|
123
|
+
return {
|
|
124
|
+
id,
|
|
125
|
+
field,
|
|
126
|
+
expression: groupExpression(field.id, groups.get(field.id)?.timeGrain),
|
|
127
|
+
column: { id, label: field.label, valueType: field.valueType },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (selection.operation !== "count" &&
|
|
131
|
+
selection.operation !== "countDistinct" &&
|
|
132
|
+
selection.operation !== "sum") {
|
|
133
|
+
throw new FinanceReportingQueryError(`Finance receivables does not support ${selection.operation}.`);
|
|
134
|
+
}
|
|
135
|
+
const field = selection.field ? requireField(definitions, selection.field) : undefined;
|
|
136
|
+
if (selection.operation === "sum" && field?.role !== "measure") {
|
|
137
|
+
throw new FinanceReportingQueryError("sum() requires a Finance measure.");
|
|
138
|
+
}
|
|
139
|
+
if (selection.operation === "countDistinct" && !field) {
|
|
140
|
+
throw new FinanceReportingQueryError("countDistinct() requires a field.");
|
|
141
|
+
}
|
|
142
|
+
if (field && !field.aggregations.includes(selection.operation)) {
|
|
143
|
+
throw new FinanceReportingQueryError(`${selection.operation} is not declared for ${JSON.stringify(field.id)}.`);
|
|
144
|
+
}
|
|
145
|
+
const expression = selection.operation === "count"
|
|
146
|
+
? field
|
|
147
|
+
? sql `count(${fieldExpression(field.id)})::bigint`
|
|
148
|
+
: sql `count(*)::bigint`
|
|
149
|
+
: selection.operation === "countDistinct"
|
|
150
|
+
? sql `count(DISTINCT ${fieldExpression(field?.id ?? "")})::bigint`
|
|
151
|
+
: sql `coalesce(sum(${fieldExpression(field?.id ?? "")}), 0)::bigint`;
|
|
152
|
+
return {
|
|
153
|
+
id: selection.as,
|
|
154
|
+
field,
|
|
155
|
+
expression,
|
|
156
|
+
column: {
|
|
157
|
+
id: selection.as,
|
|
158
|
+
label: field?.label ?? "Count",
|
|
159
|
+
valueType: selection.operation === "count" || selection.operation === "countDistinct"
|
|
160
|
+
? "integer"
|
|
161
|
+
: (field?.valueType ?? "number"),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
const aliases = new Set();
|
|
166
|
+
for (const selection of selections) {
|
|
167
|
+
if (aliases.has(selection.id)) {
|
|
168
|
+
throw new FinanceReportingQueryError(`Duplicate selection alias ${JSON.stringify(selection.id)}.`);
|
|
169
|
+
}
|
|
170
|
+
aliases.add(selection.id);
|
|
171
|
+
}
|
|
172
|
+
const moneySelections = query.select.filter((selection) => selection.field !== undefined && MONEY_FIELDS.has(selection.field));
|
|
173
|
+
const currencyIsExplicit = aggregateQuery
|
|
174
|
+
? groups.has("currency")
|
|
175
|
+
: query.select.some((selection) => selection.kind === "field" && selection.field === "currency");
|
|
176
|
+
if (moneySelections.length > 0 &&
|
|
177
|
+
!currencyIsExplicit &&
|
|
178
|
+
!hasSingleCurrencyFilter(query, parameters)) {
|
|
179
|
+
throw new FinanceReportingQueryError("Currency measures must include or group by currency, or be filtered to exactly one currency.");
|
|
180
|
+
}
|
|
181
|
+
const filters = query.filters.map((filter) => compileFilter(filter, definitions, parameters));
|
|
182
|
+
const selectSql = sql.join(selections.map(({ expression, id }) => sql `${expression} AS ${sql.identifier(id)}`), sql `, `);
|
|
183
|
+
const groupSql = [...groups.values()].map((group) => groupExpression(group.field, group.timeGrain));
|
|
184
|
+
const orderSql = query.orderBy.map((order) => {
|
|
185
|
+
if (!aliases.has(order.by)) {
|
|
186
|
+
throw new FinanceReportingQueryError(`Ordering must reference a selected output; ${JSON.stringify(order.by)} is unavailable.`);
|
|
187
|
+
}
|
|
188
|
+
return sql `${sql.identifier(order.by)} ${order.direction === "descending" ? sql `DESC` : sql `ASC`}`;
|
|
189
|
+
});
|
|
190
|
+
const rowLimit = Math.min(query.limit ?? input.maximumRows, input.maximumRows);
|
|
191
|
+
return {
|
|
192
|
+
statement: sql `
|
|
193
|
+
WITH receivable AS (${semanticReceivables})
|
|
194
|
+
SELECT ${selectSql}
|
|
195
|
+
FROM receivable
|
|
196
|
+
${filters.length ? sql `WHERE ${sql.join(filters, sql ` AND `)}` : sql ``}
|
|
197
|
+
${groupSql.length ? sql `GROUP BY ${sql.join(groupSql, sql `, `)}` : sql ``}
|
|
198
|
+
${orderSql.length ? sql `ORDER BY ${sql.join(orderSql, sql `, `)}` : sql ``}
|
|
199
|
+
LIMIT ${rowLimit + 1}
|
|
200
|
+
`,
|
|
201
|
+
columns: selections.map(({ column }) => column),
|
|
202
|
+
rowLimit,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export const financeReceivablesDataset = {
|
|
206
|
+
definition: financeReceivablesDatasetDefinition,
|
|
207
|
+
async execute(context, input) {
|
|
208
|
+
if (!hasApiKeyPermission(permissionStringsToPermissions(context.grantedScopes), "finance", "read")) {
|
|
209
|
+
throw new FinanceReportingQueryError("finance:read is required to query receivables.");
|
|
210
|
+
}
|
|
211
|
+
if (context.signal?.aborted)
|
|
212
|
+
throw abortReason(context.signal);
|
|
213
|
+
const compiled = compileFinanceReceivablesQuery(input);
|
|
214
|
+
const rows = await executeBoundaryRows(context.db, compiled.statement);
|
|
215
|
+
if (context.signal?.aborted)
|
|
216
|
+
throw abortReason(context.signal);
|
|
217
|
+
const truncated = rows.length > compiled.rowLimit;
|
|
218
|
+
return {
|
|
219
|
+
columns: compiled.columns,
|
|
220
|
+
rows: rows.slice(0, compiled.rowLimit).map((row) => normalizeRow(row, compiled.columns)),
|
|
221
|
+
truncated,
|
|
222
|
+
warnings: [],
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
function requireField(fields, id) {
|
|
227
|
+
const field = fields.get(id);
|
|
228
|
+
if (!field || !fieldSql[id]) {
|
|
229
|
+
throw new FinanceReportingQueryError(`Unknown Finance field ${JSON.stringify(id)}.`);
|
|
230
|
+
}
|
|
231
|
+
return field;
|
|
232
|
+
}
|
|
233
|
+
function fieldExpression(id) {
|
|
234
|
+
const expression = fieldSql[id];
|
|
235
|
+
if (!expression)
|
|
236
|
+
throw new FinanceReportingQueryError(`Unknown Finance field ${JSON.stringify(id)}.`);
|
|
237
|
+
return expression;
|
|
238
|
+
}
|
|
239
|
+
function groupExpression(id, grain) {
|
|
240
|
+
const expression = fieldExpression(id);
|
|
241
|
+
if (!grain)
|
|
242
|
+
return expression;
|
|
243
|
+
switch (grain) {
|
|
244
|
+
case "day":
|
|
245
|
+
return sql `date_trunc('day', ${expression}::timestamp)::date`;
|
|
246
|
+
case "week":
|
|
247
|
+
return sql `date_trunc('week', ${expression}::timestamp)::date`;
|
|
248
|
+
case "month":
|
|
249
|
+
return sql `date_trunc('month', ${expression}::timestamp)::date`;
|
|
250
|
+
case "quarter":
|
|
251
|
+
return sql `date_trunc('quarter', ${expression}::timestamp)::date`;
|
|
252
|
+
case "year":
|
|
253
|
+
return sql `date_trunc('year', ${expression}::timestamp)::date`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function compileFilter(filter, fields, parameters) {
|
|
257
|
+
const field = requireField(fields, filter.field);
|
|
258
|
+
if (field.role !== "dimension") {
|
|
259
|
+
throw new FinanceReportingQueryError(`Filtering measures is not supported.`);
|
|
260
|
+
}
|
|
261
|
+
const expression = fieldExpression(field.id);
|
|
262
|
+
if (filter.operator === "isNull")
|
|
263
|
+
return sql `${expression} IS NULL`;
|
|
264
|
+
if (filter.operator === "isNotNull")
|
|
265
|
+
return sql `${expression} IS NOT NULL`;
|
|
266
|
+
const value = requireFilterValue(filter, parameters);
|
|
267
|
+
switch (filter.operator) {
|
|
268
|
+
case "equal":
|
|
269
|
+
return value === null ? sql `${expression} IS NULL` : sql `${expression} = ${scalar(value)}`;
|
|
270
|
+
case "notEqual":
|
|
271
|
+
return value === null
|
|
272
|
+
? sql `${expression} IS NOT NULL`
|
|
273
|
+
: sql `${expression} <> ${scalar(value)}`;
|
|
274
|
+
case "greaterThan":
|
|
275
|
+
return sql `${expression} > ${scalar(value)}`;
|
|
276
|
+
case "greaterThanOrEqual":
|
|
277
|
+
return sql `${expression} >= ${scalar(value)}`;
|
|
278
|
+
case "lessThan":
|
|
279
|
+
return sql `${expression} < ${scalar(value)}`;
|
|
280
|
+
case "lessThanOrEqual":
|
|
281
|
+
return sql `${expression} <= ${scalar(value)}`;
|
|
282
|
+
case "contains": {
|
|
283
|
+
if (typeof value !== "string" || field.valueType !== "string") {
|
|
284
|
+
throw new FinanceReportingQueryError("contains requires a string dimension and value.");
|
|
285
|
+
}
|
|
286
|
+
return sql `${expression} ILIKE ${`%${value}%`}`;
|
|
287
|
+
}
|
|
288
|
+
case "in":
|
|
289
|
+
case "notIn": {
|
|
290
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
291
|
+
throw new FinanceReportingQueryError(`${filter.operator} requires a non-empty array.`);
|
|
292
|
+
}
|
|
293
|
+
const values = sql.join(value.map((entry) => sql `${scalar(entry)}`), sql `, `);
|
|
294
|
+
return filter.operator === "in"
|
|
295
|
+
? sql `${expression} IN (${values})`
|
|
296
|
+
: sql `${expression} NOT IN (${values})`;
|
|
297
|
+
}
|
|
298
|
+
case "between": {
|
|
299
|
+
if (!Array.isArray(value) || value.length !== 2) {
|
|
300
|
+
throw new FinanceReportingQueryError("between requires exactly two values.");
|
|
301
|
+
}
|
|
302
|
+
return sql `${expression} BETWEEN ${scalar(value[0])} AND ${scalar(value[1])}`;
|
|
303
|
+
}
|
|
304
|
+
default:
|
|
305
|
+
throw new FinanceReportingQueryError(`Unsupported filter operator ${filter.operator}.`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function requireFilterValue(filter, parameters) {
|
|
309
|
+
if (!filter.value)
|
|
310
|
+
throw new FinanceReportingQueryError(`${filter.operator} requires a value.`);
|
|
311
|
+
if (filter.value.kind === "literal")
|
|
312
|
+
return filter.value.value;
|
|
313
|
+
if (!(filter.value.name in parameters)) {
|
|
314
|
+
throw new FinanceReportingQueryError(`Missing query parameter ${JSON.stringify(filter.value.name)}.`);
|
|
315
|
+
}
|
|
316
|
+
const value = parameters[filter.value.name];
|
|
317
|
+
if (value === undefined) {
|
|
318
|
+
throw new FinanceReportingQueryError(`Missing query parameter ${JSON.stringify(filter.value.name)}.`);
|
|
319
|
+
}
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
322
|
+
function scalar(value) {
|
|
323
|
+
if (Array.isArray(value))
|
|
324
|
+
throw new FinanceReportingQueryError("Expected a scalar value.");
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
function hasSingleCurrencyFilter(query, parameters) {
|
|
328
|
+
return query.filters.some((filter) => {
|
|
329
|
+
if (filter.field !== "currency" || !filter.value)
|
|
330
|
+
return false;
|
|
331
|
+
const value = filter.value.kind === "literal" ? filter.value.value : parameters[filter.value.name];
|
|
332
|
+
if (filter.operator === "equal")
|
|
333
|
+
return typeof value === "string" && value.length > 0;
|
|
334
|
+
return (filter.operator === "in" &&
|
|
335
|
+
Array.isArray(value) &&
|
|
336
|
+
value.length === 1 &&
|
|
337
|
+
typeof value[0] === "string");
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function normalizeRow(row, columns) {
|
|
341
|
+
return Object.fromEntries(columns.map((column) => {
|
|
342
|
+
const value = row[column.id];
|
|
343
|
+
if (column.valueType === "date" && value instanceof Date) {
|
|
344
|
+
return [column.id, value.toISOString().slice(0, 10)];
|
|
345
|
+
}
|
|
346
|
+
if ((column.valueType === "integer" ||
|
|
347
|
+
column.valueType === "number" ||
|
|
348
|
+
column.valueType === "currency") &&
|
|
349
|
+
typeof value === "string") {
|
|
350
|
+
const numeric = Number(value);
|
|
351
|
+
if (!Number.isFinite(numeric) ||
|
|
352
|
+
((column.valueType === "integer" || column.valueType === "currency") &&
|
|
353
|
+
!Number.isSafeInteger(numeric))) {
|
|
354
|
+
throw new FinanceReportingQueryError(`Result ${JSON.stringify(column.id)} is outside the supported numeric range.`);
|
|
355
|
+
}
|
|
356
|
+
return [column.id, numeric];
|
|
357
|
+
}
|
|
358
|
+
return [column.id, value];
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
function abortReason(signal) {
|
|
362
|
+
return signal.reason ?? new Error("Finance report execution was aborted.");
|
|
363
|
+
}
|
package/dist/voyant.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
// agent-quality: file-size exception -- owner: finance; the package-owned deployment declarations remain centralized in one manifest.
|
|
1
2
|
import { actionLedgerFinanceDriftRuntimePort } from "@voyant-travel/action-ledger/runtime-port";
|
|
2
3
|
import { bookingsFinanceRuntimePort } from "@voyant-travel/bookings/runtime-port";
|
|
3
4
|
import { defineExtension, defineModule, providePort, requirePort, } from "@voyant-travel/core/project";
|
|
4
5
|
import { customFieldsRuntimePort } from "@voyant-travel/core/runtime-port";
|
|
5
6
|
import { financeAppApiRuntimePort } from "@voyant-travel/finance-contracts/runtime-port";
|
|
7
|
+
import { financeReceivablesDatasetDefinition, financeReportingTemplates, financeReportingWidgets, } from "./reporting-definitions.js";
|
|
6
8
|
import { financeAccommodationsPaymentPolicyRuntimePort, financeCheckoutPaymentStartersRuntimePort, financeCruisesPaymentPolicyRuntimePort, financeDistributionPaymentPolicyRuntimePort, financeHostRuntimePort, financeInventoryPaymentPolicyRuntimePort, financeInvoiceSettlementPollerRuntimePort, financeNotificationsRuntimePort, financeOperatorSettingsRuntimePort, } from "./runtime-port.js";
|
|
7
9
|
import { financeVoyantAdmin } from "./voyant-admin.js";
|
|
8
10
|
import { bookingContractDocumentRequestedPayloadSchema, bookingCreatedPayloadSchema, bookingCreateRejectedPayloadSchema, bookingDualCreatedPayloadSchema, bookingPaymentSchedulePaidPayloadSchema, invoiceDocumentGeneratedPayloadSchema, invoiceIssuanceExternalPayloadSchema, invoicePaymentRecordedExternalPayloadSchema, invoiceProformaConvertedExternalPayloadSchema, invoiceRenderedPayloadSchema, invoiceSettledPayloadSchema, invoiceVoidedExternalPayloadSchema, paymentCompletedPayloadSchema, } from "./voyant-event-schemas.js";
|
|
@@ -68,6 +70,64 @@ export const financeVoyantModule = defineModule({
|
|
|
68
70
|
source: "@voyant-travel/finance/schema",
|
|
69
71
|
},
|
|
70
72
|
],
|
|
73
|
+
reporting: {
|
|
74
|
+
datasets: [
|
|
75
|
+
{
|
|
76
|
+
id: financeReceivablesDatasetDefinition.id,
|
|
77
|
+
version: financeReceivablesDatasetDefinition.version,
|
|
78
|
+
label: financeReceivablesDatasetDefinition.label,
|
|
79
|
+
description: financeReceivablesDatasetDefinition.description,
|
|
80
|
+
descriptor: financeReceivablesDatasetDefinition,
|
|
81
|
+
requiredScopes: financeReceivablesDatasetDefinition.requiredScopes,
|
|
82
|
+
runtime: {
|
|
83
|
+
entry: "@voyant-travel/finance/reporting",
|
|
84
|
+
export: "financeReceivablesDataset",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
widgets: financeReportingWidgets.map((widget) => ({
|
|
89
|
+
id: widget.id,
|
|
90
|
+
version: widget.version,
|
|
91
|
+
label: widget.label,
|
|
92
|
+
description: widget.description,
|
|
93
|
+
datasetId: widget.query.dataset.id,
|
|
94
|
+
...(widget.query.dataset.version ? { datasetVersion: widget.query.dataset.version } : {}),
|
|
95
|
+
query: {
|
|
96
|
+
select: widget.query.select,
|
|
97
|
+
filters: widget.query.filters,
|
|
98
|
+
groupBy: widget.query.groupBy,
|
|
99
|
+
orderBy: widget.query.orderBy,
|
|
100
|
+
...(widget.query.limit ? { limit: widget.query.limit } : {}),
|
|
101
|
+
},
|
|
102
|
+
visualization: {
|
|
103
|
+
type: widget.visualization.type,
|
|
104
|
+
options: omitUndefinedJsonOptions(widget.visualization.options),
|
|
105
|
+
},
|
|
106
|
+
defaultSize: widget.defaultSize,
|
|
107
|
+
...(widget.minimumSize ? { minSize: widget.minimumSize } : {}),
|
|
108
|
+
...(widget.maximumSize ? { maxSize: widget.maximumSize } : {}),
|
|
109
|
+
})),
|
|
110
|
+
templates: financeReportingTemplates.map((template) => ({
|
|
111
|
+
id: template.id,
|
|
112
|
+
version: template.version,
|
|
113
|
+
label: template.label,
|
|
114
|
+
description: template.description,
|
|
115
|
+
requirements: template.widgets.map((widget) => ({
|
|
116
|
+
kind: "widget",
|
|
117
|
+
id: widget.source.kind === "preset" ? widget.source.widgetId : widget.id,
|
|
118
|
+
})),
|
|
119
|
+
widgets: template.widgets.flatMap((widget) => widget.source.kind === "preset"
|
|
120
|
+
? [
|
|
121
|
+
{
|
|
122
|
+
id: widget.id,
|
|
123
|
+
widgetId: widget.source.widgetId,
|
|
124
|
+
...(widget.source.version ? { widgetVersion: widget.source.version } : {}),
|
|
125
|
+
layout: widget.layout,
|
|
126
|
+
},
|
|
127
|
+
]
|
|
128
|
+
: []),
|
|
129
|
+
})),
|
|
130
|
+
},
|
|
71
131
|
migrations: [
|
|
72
132
|
{
|
|
73
133
|
id: "@voyant-travel/finance#migrations",
|
|
@@ -566,4 +626,12 @@ export const financeBookingScheduleVoyantPlugin = defineExtension({
|
|
|
566
626
|
ownership: "package",
|
|
567
627
|
},
|
|
568
628
|
});
|
|
629
|
+
function omitUndefinedJsonOptions(options) {
|
|
630
|
+
const jsonOptions = {};
|
|
631
|
+
for (const [key, value] of Object.entries(options)) {
|
|
632
|
+
if (value !== undefined)
|
|
633
|
+
jsonOptions[key] = value;
|
|
634
|
+
}
|
|
635
|
+
return jsonOptions;
|
|
636
|
+
}
|
|
569
637
|
export default financeVoyantModule;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/finance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.175.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
"import": "./dist/invoice-fx.js",
|
|
45
45
|
"default": "./dist/invoice-fx.js"
|
|
46
46
|
},
|
|
47
|
+
"./reporting": {
|
|
48
|
+
"types": "./dist/reporting.d.ts",
|
|
49
|
+
"import": "./dist/reporting.js",
|
|
50
|
+
"default": "./dist/reporting.js"
|
|
51
|
+
},
|
|
47
52
|
"./order-payment-sessions": {
|
|
48
53
|
"types": "./dist/order-payment-sessions.d.ts",
|
|
49
54
|
"import": "./dist/order-payment-sessions.js",
|
|
@@ -151,23 +156,24 @@
|
|
|
151
156
|
"fflate": "^0.8.2",
|
|
152
157
|
"hono": "^4.12.27",
|
|
153
158
|
"zod": "^4.4.3",
|
|
154
|
-
"@voyant-travel/action-ledger": "^0.111.
|
|
155
|
-
"@voyant-travel/bookings": "^0.
|
|
156
|
-
"@voyant-travel/core": "^0.
|
|
157
|
-
"@voyant-travel/db": "^0.
|
|
158
|
-
"@voyant-travel/types": "^0.109.
|
|
159
|
+
"@voyant-travel/action-ledger": "^0.111.7",
|
|
160
|
+
"@voyant-travel/bookings": "^0.175.0",
|
|
161
|
+
"@voyant-travel/core": "^0.130.0",
|
|
162
|
+
"@voyant-travel/db": "^0.115.0",
|
|
163
|
+
"@voyant-travel/types": "^0.109.6",
|
|
159
164
|
"@voyant-travel/finance-contracts": "^0.107.0",
|
|
160
|
-
"@voyant-travel/hono": "^0.
|
|
161
|
-
"@voyant-travel/payments": "^0.2.
|
|
162
|
-
"@voyant-travel/public-document-delivery": "^0.4.
|
|
163
|
-
"@voyant-travel/
|
|
165
|
+
"@voyant-travel/hono": "^0.131.0",
|
|
166
|
+
"@voyant-travel/payments": "^0.2.6",
|
|
167
|
+
"@voyant-travel/public-document-delivery": "^0.4.6",
|
|
168
|
+
"@voyant-travel/reporting-contracts": "^0.2.1",
|
|
169
|
+
"@voyant-travel/storage": "^0.111.6",
|
|
164
170
|
"@voyant-travel/utils": "^0.108.0",
|
|
165
171
|
"@voyant-travel/tools": "^0.3.0"
|
|
166
172
|
},
|
|
167
173
|
"devDependencies": {
|
|
168
174
|
"drizzle-kit": "^0.31.10",
|
|
169
175
|
"typescript": "^6.0.3",
|
|
170
|
-
"@voyant-travel/webhook-delivery": "^0.4.
|
|
176
|
+
"@voyant-travel/webhook-delivery": "^0.4.5",
|
|
171
177
|
"@voyant-travel/voyant-typescript-config": "^0.1.0"
|
|
172
178
|
},
|
|
173
179
|
"files": [
|