@yolk-sdk/emulators 0.1.0-canary.96
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/LICENSE +21 -0
- package/README.md +2135 -0
- package/dist/anthropic.d.mts +269 -0
- package/dist/anthropic.d.mts.map +1 -0
- package/dist/anthropic.mjs +177 -0
- package/dist/anthropic.mjs.map +1 -0
- package/dist/chat-completions.d.mts +288 -0
- package/dist/chat-completions.d.mts.map +1 -0
- package/dist/chat-completions.mjs +451 -0
- package/dist/chat-completions.mjs.map +1 -0
- package/dist/codex.d.mts +265 -0
- package/dist/codex.d.mts.map +1 -0
- package/dist/codex.mjs +199 -0
- package/dist/codex.mjs.map +1 -0
- package/dist/dropbox/api.d.mts +63 -0
- package/dist/dropbox/api.d.mts.map +1 -0
- package/dist/dropbox/api.mjs +565 -0
- package/dist/dropbox/api.mjs.map +1 -0
- package/dist/dropbox/state.d.mts +136 -0
- package/dist/dropbox/state.d.mts.map +1 -0
- package/dist/dropbox/state.mjs +209 -0
- package/dist/dropbox/state.mjs.map +1 -0
- package/dist/dropbox.d.mts +79 -0
- package/dist/dropbox.d.mts.map +1 -0
- package/dist/dropbox.mjs +124 -0
- package/dist/dropbox.mjs.map +1 -0
- package/dist/email-fixtures.d.mts +36 -0
- package/dist/email-fixtures.d.mts.map +1 -0
- package/dist/email-fixtures.mjs +1080 -0
- package/dist/email-fixtures.mjs.map +1 -0
- package/dist/email.d.mts +160 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +608 -0
- package/dist/email.mjs.map +1 -0
- package/dist/emulator-compose.d.mts +40 -0
- package/dist/emulator-compose.d.mts.map +1 -0
- package/dist/emulator-compose.mjs +91 -0
- package/dist/emulator-compose.mjs.map +1 -0
- package/dist/emulator-http.d.mts +53 -0
- package/dist/emulator-http.d.mts.map +1 -0
- package/dist/emulator-http.mjs +117 -0
- package/dist/emulator-http.mjs.map +1 -0
- package/dist/emulator-kernel.d.mts +176 -0
- package/dist/emulator-kernel.d.mts.map +1 -0
- package/dist/emulator-kernel.mjs +413 -0
- package/dist/emulator-kernel.mjs.map +1 -0
- package/dist/fixture-route.d.mts +129 -0
- package/dist/fixture-route.d.mts.map +1 -0
- package/dist/fixture-route.mjs +257 -0
- package/dist/fixture-route.mjs.map +1 -0
- package/dist/fortnox/api.d.mts +92 -0
- package/dist/fortnox/api.d.mts.map +1 -0
- package/dist/fortnox/api.mjs +750 -0
- package/dist/fortnox/api.mjs.map +1 -0
- package/dist/fortnox/state.d.mts +533 -0
- package/dist/fortnox/state.d.mts.map +1 -0
- package/dist/fortnox/state.mjs +612 -0
- package/dist/fortnox/state.mjs.map +1 -0
- package/dist/fortnox.d.mts +128 -0
- package/dist/fortnox.d.mts.map +1 -0
- package/dist/fortnox.mjs +403 -0
- package/dist/fortnox.mjs.map +1 -0
- package/dist/gateway-evaluate-recordings.d.mts +12 -0
- package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
- package/dist/gateway-evaluate-recordings.mjs +128 -0
- package/dist/gateway-evaluate-recordings.mjs.map +1 -0
- package/dist/gateway.d.mts +272 -0
- package/dist/gateway.d.mts.map +1 -0
- package/dist/gateway.mjs +400 -0
- package/dist/gateway.mjs.map +1 -0
- package/dist/github/api.d.mts +63 -0
- package/dist/github/api.d.mts.map +1 -0
- package/dist/github/api.mjs +527 -0
- package/dist/github/api.mjs.map +1 -0
- package/dist/github/state.d.mts +272 -0
- package/dist/github/state.d.mts.map +1 -0
- package/dist/github/state.mjs +303 -0
- package/dist/github/state.mjs.map +1 -0
- package/dist/github.d.mts +77 -0
- package/dist/github.d.mts.map +1 -0
- package/dist/github.mjs +180 -0
- package/dist/github.mjs.map +1 -0
- package/dist/google/calendar.d.mts +10 -0
- package/dist/google/calendar.d.mts.map +1 -0
- package/dist/google/calendar.mjs +252 -0
- package/dist/google/calendar.mjs.map +1 -0
- package/dist/google/drive.d.mts +8 -0
- package/dist/google/drive.d.mts.map +1 -0
- package/dist/google/drive.mjs +252 -0
- package/dist/google/drive.mjs.map +1 -0
- package/dist/google/gmail.d.mts +10 -0
- package/dist/google/gmail.d.mts.map +1 -0
- package/dist/google/gmail.mjs +591 -0
- package/dist/google/gmail.mjs.map +1 -0
- package/dist/google/shared.d.mts +107 -0
- package/dist/google/shared.d.mts.map +1 -0
- package/dist/google/shared.mjs +146 -0
- package/dist/google/shared.mjs.map +1 -0
- package/dist/google/state.d.mts +385 -0
- package/dist/google/state.d.mts.map +1 -0
- package/dist/google/state.mjs +496 -0
- package/dist/google/state.mjs.map +1 -0
- package/dist/google.d.mts +75 -0
- package/dist/google.d.mts.map +1 -0
- package/dist/google.mjs +210 -0
- package/dist/google.mjs.map +1 -0
- package/dist/linkedin-search/api.d.mts +45 -0
- package/dist/linkedin-search/api.d.mts.map +1 -0
- package/dist/linkedin-search/api.mjs +181 -0
- package/dist/linkedin-search/api.mjs.map +1 -0
- package/dist/linkedin-search/state.d.mts +123 -0
- package/dist/linkedin-search/state.d.mts.map +1 -0
- package/dist/linkedin-search/state.mjs +261 -0
- package/dist/linkedin-search/state.mjs.map +1 -0
- package/dist/linkedin-search.d.mts +69 -0
- package/dist/linkedin-search.d.mts.map +1 -0
- package/dist/linkedin-search.mjs +158 -0
- package/dist/linkedin-search.mjs.map +1 -0
- package/dist/mcp/api.d.mts +44 -0
- package/dist/mcp/api.d.mts.map +1 -0
- package/dist/mcp/api.mjs +557 -0
- package/dist/mcp/api.mjs.map +1 -0
- package/dist/mcp/recordings.d.mts +40 -0
- package/dist/mcp/recordings.d.mts.map +1 -0
- package/dist/mcp/recordings.mjs +2390 -0
- package/dist/mcp/recordings.mjs.map +1 -0
- package/dist/mcp/state.d.mts +71 -0
- package/dist/mcp/state.d.mts.map +1 -0
- package/dist/mcp/state.mjs +83 -0
- package/dist/mcp/state.mjs.map +1 -0
- package/dist/mcp.d.mts +84 -0
- package/dist/mcp.d.mts.map +1 -0
- package/dist/mcp.mjs +241 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/messages.d.mts +183 -0
- package/dist/messages.d.mts.map +1 -0
- package/dist/messages.mjs +532 -0
- package/dist/messages.mjs.map +1 -0
- package/dist/microsoft/api.d.mts +47 -0
- package/dist/microsoft/api.d.mts.map +1 -0
- package/dist/microsoft/api.mjs +178 -0
- package/dist/microsoft/api.mjs.map +1 -0
- package/dist/microsoft/calendar.d.mts +30 -0
- package/dist/microsoft/calendar.d.mts.map +1 -0
- package/dist/microsoft/calendar.mjs +271 -0
- package/dist/microsoft/calendar.mjs.map +1 -0
- package/dist/microsoft/drive.d.mts +36 -0
- package/dist/microsoft/drive.d.mts.map +1 -0
- package/dist/microsoft/drive.mjs +298 -0
- package/dist/microsoft/drive.mjs.map +1 -0
- package/dist/microsoft/graph.d.mts +198 -0
- package/dist/microsoft/graph.d.mts.map +1 -0
- package/dist/microsoft/graph.mjs +264 -0
- package/dist/microsoft/graph.mjs.map +1 -0
- package/dist/microsoft/mail.d.mts +48 -0
- package/dist/microsoft/mail.d.mts.map +1 -0
- package/dist/microsoft/mail.mjs +488 -0
- package/dist/microsoft/mail.mjs.map +1 -0
- package/dist/microsoft/state.d.mts +480 -0
- package/dist/microsoft/state.d.mts.map +1 -0
- package/dist/microsoft/state.mjs +625 -0
- package/dist/microsoft/state.mjs.map +1 -0
- package/dist/microsoft.d.mts +168 -0
- package/dist/microsoft.d.mts.map +1 -0
- package/dist/microsoft.mjs +483 -0
- package/dist/microsoft.mjs.map +1 -0
- package/dist/node.d.mts +47 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +178 -0
- package/dist/node.mjs.map +1 -0
- package/dist/notion/api.d.mts +51 -0
- package/dist/notion/api.d.mts.map +1 -0
- package/dist/notion/api.mjs +507 -0
- package/dist/notion/api.mjs.map +1 -0
- package/dist/notion/state.d.mts +329 -0
- package/dist/notion/state.d.mts.map +1 -0
- package/dist/notion/state.mjs +432 -0
- package/dist/notion/state.mjs.map +1 -0
- package/dist/notion.d.mts +72 -0
- package/dist/notion.d.mts.map +1 -0
- package/dist/notion.mjs +127 -0
- package/dist/notion.mjs.map +1 -0
- package/dist/openai.d.mts +165 -0
- package/dist/openai.d.mts.map +1 -0
- package/dist/openai.mjs +141 -0
- package/dist/openai.mjs.map +1 -0
- package/dist/opencode-recordings.d.mts +7 -0
- package/dist/opencode-recordings.d.mts.map +1 -0
- package/dist/opencode-recordings.mjs +222 -0
- package/dist/opencode-recordings.mjs.map +1 -0
- package/dist/opencode.d.mts +70 -0
- package/dist/opencode.d.mts.map +1 -0
- package/dist/opencode.mjs +217 -0
- package/dist/opencode.mjs.map +1 -0
- package/dist/r2-fixtures.d.mts +36 -0
- package/dist/r2-fixtures.d.mts.map +1 -0
- package/dist/r2-fixtures.mjs +245 -0
- package/dist/r2-fixtures.mjs.map +1 -0
- package/dist/r2-guard.d.mts +43 -0
- package/dist/r2-guard.d.mts.map +1 -0
- package/dist/r2-guard.mjs +215 -0
- package/dist/r2-guard.mjs.map +1 -0
- package/dist/r2.d.mts +166 -0
- package/dist/r2.d.mts.map +1 -0
- package/dist/r2.mjs +517 -0
- package/dist/r2.mjs.map +1 -0
- package/dist/responses.d.mts +231 -0
- package/dist/responses.d.mts.map +1 -0
- package/dist/responses.mjs +556 -0
- package/dist/responses.mjs.map +1 -0
- package/dist/route-evidence.d.mts +45 -0
- package/dist/route-evidence.d.mts.map +1 -0
- package/dist/route-evidence.mjs +56 -0
- package/dist/route-evidence.mjs.map +1 -0
- package/dist/router.d.mts +93 -0
- package/dist/router.d.mts.map +1 -0
- package/dist/router.mjs +259 -0
- package/dist/router.mjs.map +1 -0
- package/dist/stateful-core.d.mts +17 -0
- package/dist/stateful-core.d.mts.map +1 -0
- package/dist/stateful-core.mjs +41 -0
- package/dist/stateful-core.mjs.map +1 -0
- package/dist/stateful-emulator.d.mts +565 -0
- package/dist/stateful-emulator.d.mts.map +1 -0
- package/dist/stateful-emulator.mjs +1228 -0
- package/dist/stateful-emulator.mjs.map +1 -0
- package/dist/stateful-secrets.d.mts +84 -0
- package/dist/stateful-secrets.d.mts.map +1 -0
- package/dist/stateful-secrets.mjs +216 -0
- package/dist/stateful-secrets.mjs.map +1 -0
- package/dist/subscription-usage-recordings.d.mts +9 -0
- package/dist/subscription-usage-recordings.d.mts.map +1 -0
- package/dist/subscription-usage-recordings.mjs +53 -0
- package/dist/subscription-usage-recordings.mjs.map +1 -0
- package/dist/subscription-usage.d.mts +53 -0
- package/dist/subscription-usage.d.mts.map +1 -0
- package/dist/subscription-usage.mjs +21 -0
- package/dist/subscription-usage.mjs.map +1 -0
- package/dist/telegram/api.d.mts +34 -0
- package/dist/telegram/api.d.mts.map +1 -0
- package/dist/telegram/api.mjs +257 -0
- package/dist/telegram/api.mjs.map +1 -0
- package/dist/telegram/state.d.mts +128 -0
- package/dist/telegram/state.d.mts.map +1 -0
- package/dist/telegram/state.mjs +154 -0
- package/dist/telegram/state.mjs.map +1 -0
- package/dist/telegram.d.mts +62 -0
- package/dist/telegram.d.mts.map +1 -0
- package/dist/telegram.mjs +127 -0
- package/dist/telegram.mjs.map +1 -0
- package/dist/todoist/api.d.mts +55 -0
- package/dist/todoist/api.d.mts.map +1 -0
- package/dist/todoist/api.mjs +415 -0
- package/dist/todoist/api.mjs.map +1 -0
- package/dist/todoist/state.d.mts +275 -0
- package/dist/todoist/state.d.mts.map +1 -0
- package/dist/todoist/state.mjs +345 -0
- package/dist/todoist/state.mjs.map +1 -0
- package/dist/todoist.d.mts +68 -0
- package/dist/todoist.d.mts.map +1 -0
- package/dist/todoist.mjs +181 -0
- package/dist/todoist.mjs.map +1 -0
- package/dist/xai.d.mts +267 -0
- package/dist/xai.d.mts.map +1 -0
- package/dist/xai.mjs +252 -0
- package/dist/xai.mjs.map +1 -0
- package/package.json +157 -0
- package/src/anthropic.ts +289 -0
- package/src/chat-completions.ts +924 -0
- package/src/codex.ts +296 -0
- package/src/dropbox/api.ts +1084 -0
- package/src/dropbox/state.ts +364 -0
- package/src/dropbox.ts +203 -0
- package/src/email-fixtures.ts +1297 -0
- package/src/email.ts +1108 -0
- package/src/emulator-compose.ts +147 -0
- package/src/emulator-http.ts +184 -0
- package/src/emulator-kernel.ts +844 -0
- package/src/fixture-route.ts +561 -0
- package/src/fortnox/api.ts +1352 -0
- package/src/fortnox/state.ts +798 -0
- package/src/fortnox.ts +801 -0
- package/src/gateway-evaluate-recordings.ts +153 -0
- package/src/gateway.ts +530 -0
- package/src/github/api.ts +986 -0
- package/src/github/state.ts +439 -0
- package/src/github.ts +271 -0
- package/src/google/calendar.ts +486 -0
- package/src/google/drive.ts +471 -0
- package/src/google/gmail.ts +1011 -0
- package/src/google/shared.ts +321 -0
- package/src/google/state.ts +686 -0
- package/src/google.ts +298 -0
- package/src/linkedin-search/api.ts +363 -0
- package/src/linkedin-search/state.ts +373 -0
- package/src/linkedin-search.ts +241 -0
- package/src/mcp/api.ts +995 -0
- package/src/mcp/recordings.ts +2665 -0
- package/src/mcp/state.ts +125 -0
- package/src/mcp.ts +319 -0
- package/src/messages.ts +844 -0
- package/src/microsoft/api.ts +426 -0
- package/src/microsoft/calendar.ts +491 -0
- package/src/microsoft/drive.ts +541 -0
- package/src/microsoft/graph.ts +550 -0
- package/src/microsoft/mail.ts +833 -0
- package/src/microsoft/state.ts +822 -0
- package/src/microsoft.ts +982 -0
- package/src/node.ts +293 -0
- package/src/notion/api.ts +976 -0
- package/src/notion/state.ts +512 -0
- package/src/notion.ts +210 -0
- package/src/openai.ts +198 -0
- package/src/opencode-recordings.ts +257 -0
- package/src/opencode.ts +271 -0
- package/src/r2-fixtures.ts +295 -0
- package/src/r2-guard.ts +323 -0
- package/src/r2.ts +890 -0
- package/src/responses.ts +901 -0
- package/src/route-evidence.ts +90 -0
- package/src/router.ts +490 -0
- package/src/stateful-core.ts +70 -0
- package/src/stateful-emulator.ts +2571 -0
- package/src/stateful-secrets.ts +299 -0
- package/src/subscription-usage-recordings.ts +77 -0
- package/src/subscription-usage.ts +68 -0
- package/src/telegram/api.ts +437 -0
- package/src/telegram/state.ts +229 -0
- package/src/telegram.ts +227 -0
- package/src/todoist/api.ts +736 -0
- package/src/todoist/state.ts +456 -0
- package/src/todoist.ts +316 -0
- package/src/xai.ts +341 -0
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fortnox emulator state: the typed entities, the seed input, the default seed, and the
|
|
3
|
+
* account-variance profiles (internal; re-exported by `src/fortnox.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Entity shapes and the default entities follow the synthetic Fortnox conformance fixtures, copied
|
|
6
|
+
* as data (the same customer and document numbers as `fortnoxConformanceFixtureSeeds`), never
|
|
7
|
+
* imported from SDK code. Where no fixture records a value, the default is synthesized and says
|
|
8
|
+
* so below: customer 1002, most invoice rows, and the company information have no fixture.
|
|
9
|
+
*
|
|
10
|
+
* @experimental
|
|
11
|
+
*/
|
|
12
|
+
import { Result, Struct } from 'effect'
|
|
13
|
+
import * as Schema from 'effect/Schema'
|
|
14
|
+
|
|
15
|
+
const DiscountType = Schema.Literals(['PERCENT', 'AMOUNT'])
|
|
16
|
+
|
|
17
|
+
const rowFields = {
|
|
18
|
+
RowId: Schema.Int,
|
|
19
|
+
AccountNumber: Schema.NullOr(Schema.Int),
|
|
20
|
+
ArticleNumber: Schema.String,
|
|
21
|
+
ContributionPercent: Schema.String,
|
|
22
|
+
ContributionValue: Schema.String,
|
|
23
|
+
CostCenter: Schema.String,
|
|
24
|
+
DeliveredQuantity: Schema.String,
|
|
25
|
+
Description: Schema.String,
|
|
26
|
+
Discount: Schema.Finite,
|
|
27
|
+
DiscountType,
|
|
28
|
+
HouseWork: Schema.Boolean,
|
|
29
|
+
Price: Schema.Finite,
|
|
30
|
+
PriceExcludingVAT: Schema.Finite,
|
|
31
|
+
Project: Schema.String,
|
|
32
|
+
Total: Schema.Finite,
|
|
33
|
+
TotalExcludingVAT: Schema.Finite,
|
|
34
|
+
Unit: Schema.String,
|
|
35
|
+
VAT: Schema.Finite,
|
|
36
|
+
VATCode: Schema.String
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A stored invoice row, in the single-invoice wire shape. Totals are derived. */
|
|
40
|
+
export const FortnoxEmulatorInvoiceRow = Schema.Struct(rowFields)
|
|
41
|
+
|
|
42
|
+
export type FortnoxEmulatorInvoiceRow = typeof FortnoxEmulatorInvoiceRow.Type
|
|
43
|
+
|
|
44
|
+
const EmailInformation = Schema.Struct({
|
|
45
|
+
EmailAddressFrom: Schema.String,
|
|
46
|
+
EmailAddressTo: Schema.String,
|
|
47
|
+
EmailAddressCC: Schema.String,
|
|
48
|
+
EmailAddressBCC: Schema.String,
|
|
49
|
+
EmailSubject: Schema.String,
|
|
50
|
+
EmailBody: Schema.String
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const invoiceFields = {
|
|
54
|
+
DocumentNumber: Schema.String,
|
|
55
|
+
CustomerNumber: Schema.String,
|
|
56
|
+
CustomerName: Schema.String,
|
|
57
|
+
Balance: Schema.Finite,
|
|
58
|
+
Booked: Schema.Boolean,
|
|
59
|
+
Cancelled: Schema.Boolean,
|
|
60
|
+
Comments: Schema.String,
|
|
61
|
+
CostCenter: Schema.String,
|
|
62
|
+
Credit: Schema.String,
|
|
63
|
+
Currency: Schema.String,
|
|
64
|
+
CurrencyRate: Schema.Finite,
|
|
65
|
+
CurrencyUnit: Schema.Finite,
|
|
66
|
+
DueDate: Schema.NullOr(Schema.String),
|
|
67
|
+
EmailInformation,
|
|
68
|
+
ExternalInvoiceReference1: Schema.String,
|
|
69
|
+
ExternalInvoiceReference2: Schema.String,
|
|
70
|
+
FinalPayDate: Schema.NullOr(Schema.String),
|
|
71
|
+
Gross: Schema.Finite,
|
|
72
|
+
InvoiceDate: Schema.String,
|
|
73
|
+
InvoiceRows: Schema.Array(FortnoxEmulatorInvoiceRow),
|
|
74
|
+
InvoiceType: Schema.String,
|
|
75
|
+
Net: Schema.Finite,
|
|
76
|
+
NotCompleted: Schema.Boolean,
|
|
77
|
+
NoxFinans: Schema.Boolean,
|
|
78
|
+
OCR: Schema.String,
|
|
79
|
+
OurReference: Schema.String,
|
|
80
|
+
Project: Schema.String,
|
|
81
|
+
Sent: Schema.Boolean,
|
|
82
|
+
TermsOfPayment: Schema.String,
|
|
83
|
+
Total: Schema.Finite,
|
|
84
|
+
TotalToPay: Schema.Finite,
|
|
85
|
+
TotalVAT: Schema.Finite,
|
|
86
|
+
VoucherNumber: Schema.NullOr(Schema.Int),
|
|
87
|
+
VoucherSeries: Schema.NullOr(Schema.String),
|
|
88
|
+
VoucherYear: Schema.NullOr(Schema.Int),
|
|
89
|
+
WayOfDelivery: Schema.String,
|
|
90
|
+
YourReference: Schema.String
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A stored customer invoice with its rows. */
|
|
94
|
+
export const FortnoxEmulatorInvoice = Schema.Struct(invoiceFields)
|
|
95
|
+
|
|
96
|
+
export type FortnoxEmulatorInvoice = typeof FortnoxEmulatorInvoice.Type
|
|
97
|
+
|
|
98
|
+
const customerFields = {
|
|
99
|
+
CustomerNumber: Schema.String,
|
|
100
|
+
Name: Schema.String,
|
|
101
|
+
Active: Schema.Boolean,
|
|
102
|
+
Address1: Schema.String,
|
|
103
|
+
Address2: Schema.String,
|
|
104
|
+
City: Schema.String,
|
|
105
|
+
Comments: Schema.String,
|
|
106
|
+
CostCenter: Schema.String,
|
|
107
|
+
Country: Schema.String,
|
|
108
|
+
CountryCode: Schema.String,
|
|
109
|
+
Currency: Schema.String,
|
|
110
|
+
Email: Schema.String,
|
|
111
|
+
EmailInvoice: Schema.String,
|
|
112
|
+
OrganisationNumber: Schema.String,
|
|
113
|
+
OurReference: Schema.String,
|
|
114
|
+
Phone1: Schema.String,
|
|
115
|
+
Phone2: Schema.String,
|
|
116
|
+
TermsOfPayment: Schema.String,
|
|
117
|
+
Type: Schema.String,
|
|
118
|
+
VATNumber: Schema.String,
|
|
119
|
+
VATType: Schema.String,
|
|
120
|
+
YourReference: Schema.String,
|
|
121
|
+
ZipCode: Schema.String
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A stored customer. `Country` is read-only (derived from `CountryCode`). */
|
|
125
|
+
export const FortnoxEmulatorCustomer = Schema.Struct(customerFields)
|
|
126
|
+
|
|
127
|
+
export type FortnoxEmulatorCustomer = typeof FortnoxEmulatorCustomer.Type
|
|
128
|
+
|
|
129
|
+
const companyFields = {
|
|
130
|
+
CompanyName: Schema.String,
|
|
131
|
+
OrganizationNumber: Schema.String,
|
|
132
|
+
DatabaseNumber: Schema.Int,
|
|
133
|
+
Address: Schema.String,
|
|
134
|
+
City: Schema.String,
|
|
135
|
+
ZipCode: Schema.String,
|
|
136
|
+
CountryCode: Schema.String,
|
|
137
|
+
VisitAddress: Schema.String,
|
|
138
|
+
VisitCity: Schema.String,
|
|
139
|
+
VisitZipCode: Schema.String,
|
|
140
|
+
VisitCountryCode: Schema.String
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The company the credential belongs to (`GET /3/companyinformation`). */
|
|
144
|
+
export const FortnoxEmulatorCompany = Schema.Struct(companyFields)
|
|
145
|
+
|
|
146
|
+
export type FortnoxEmulatorCompany = typeof FortnoxEmulatorCompany.Type
|
|
147
|
+
|
|
148
|
+
/** A recorded (never delivered) invoice email. */
|
|
149
|
+
export const FortnoxEmulatorOutboxEntry = Schema.Struct({
|
|
150
|
+
DocumentNumber: Schema.String,
|
|
151
|
+
EmailAddressTo: Schema.String,
|
|
152
|
+
EmailAddressCC: Schema.String,
|
|
153
|
+
EmailAddressBCC: Schema.String,
|
|
154
|
+
EmailSubject: Schema.String,
|
|
155
|
+
/** ISO timestamp from the emulator clock. */
|
|
156
|
+
sentAt: Schema.String
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
export type FortnoxEmulatorOutboxEntry = typeof FortnoxEmulatorOutboxEntry.Type
|
|
160
|
+
|
|
161
|
+
const Counter = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
|
|
162
|
+
|
|
163
|
+
const Counters = Schema.Struct({
|
|
164
|
+
/** Next RowId handed out; RowIds are regenerated on every row update. */
|
|
165
|
+
nextRowId: Counter,
|
|
166
|
+
/** Next DocumentNumber handed out by `POST /3/invoices`. */
|
|
167
|
+
nextDocumentNumber: Counter
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
/** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
|
|
171
|
+
export const FortnoxEmulatorStateSchema = Schema.Struct({
|
|
172
|
+
company: FortnoxEmulatorCompany,
|
|
173
|
+
customers: Schema.Array(FortnoxEmulatorCustomer),
|
|
174
|
+
invoices: Schema.Array(FortnoxEmulatorInvoice),
|
|
175
|
+
outbox: Schema.Array(FortnoxEmulatorOutboxEntry),
|
|
176
|
+
counters: Counters
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The emulator state. The container is mutable (route handlers replace whole entity lists);
|
|
181
|
+
* every entity is replaced, never edited in place.
|
|
182
|
+
*/
|
|
183
|
+
export type FortnoxEmulatorState = {
|
|
184
|
+
company: FortnoxEmulatorCompany
|
|
185
|
+
customers: ReadonlyArray<FortnoxEmulatorCustomer>
|
|
186
|
+
invoices: ReadonlyArray<FortnoxEmulatorInvoice>
|
|
187
|
+
outbox: ReadonlyArray<FortnoxEmulatorOutboxEntry>
|
|
188
|
+
counters: typeof Counters.Type
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** A seeded invoice row: the writable row fields; totals and `VATCode` are derived. */
|
|
192
|
+
export const FortnoxEmulatorInvoiceRowSeed = Schema.Struct({
|
|
193
|
+
RowId: Schema.optionalKey(Schema.Int),
|
|
194
|
+
AccountNumber: Schema.optionalKey(Schema.NullOr(Schema.Int)),
|
|
195
|
+
ArticleNumber: Schema.optionalKey(Schema.String),
|
|
196
|
+
CostCenter: Schema.optionalKey(Schema.String),
|
|
197
|
+
DeliveredQuantity: Schema.optionalKey(Schema.String),
|
|
198
|
+
Description: Schema.optionalKey(Schema.String),
|
|
199
|
+
Discount: Schema.optionalKey(Schema.Finite),
|
|
200
|
+
DiscountType: Schema.optionalKey(DiscountType),
|
|
201
|
+
Price: Schema.optionalKey(Schema.Finite),
|
|
202
|
+
Project: Schema.optionalKey(Schema.String),
|
|
203
|
+
Unit: Schema.optionalKey(Schema.String),
|
|
204
|
+
VAT: Schema.optionalKey(Schema.Finite)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
export type FortnoxEmulatorInvoiceRowSeed = typeof FortnoxEmulatorInvoiceRowSeed.Type
|
|
208
|
+
|
|
209
|
+
const { InvoiceRows: _invoiceRows, ...invoiceScalarFields } = invoiceFields
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A seeded invoice: `DocumentNumber` and `CustomerNumber` are required; everything else has a
|
|
213
|
+
* synthetic default. Totals (`Net`, `Gross`, `TotalVAT`, `Total`, `TotalToPay`) are always
|
|
214
|
+
* computed from the rows; `Balance` defaults to `Total`.
|
|
215
|
+
*/
|
|
216
|
+
export const FortnoxEmulatorInvoiceSeed = Schema.Struct({
|
|
217
|
+
...Struct.mapOmit(invoiceScalarFields, ['DocumentNumber', 'CustomerNumber'], Schema.optionalKey),
|
|
218
|
+
InvoiceRows: Schema.optionalKey(Schema.Array(FortnoxEmulatorInvoiceRowSeed))
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
export type FortnoxEmulatorInvoiceSeed = typeof FortnoxEmulatorInvoiceSeed.Type
|
|
222
|
+
|
|
223
|
+
/** A seeded customer: `CustomerNumber` and `Name` are required; `Country` follows `CountryCode`. */
|
|
224
|
+
export const FortnoxEmulatorCustomerSeed = Schema.Struct(
|
|
225
|
+
Struct.mapOmit(customerFields, ['CustomerNumber', 'Name'], Schema.optionalKey)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
export type FortnoxEmulatorCustomerSeed = typeof FortnoxEmulatorCustomerSeed.Type
|
|
229
|
+
|
|
230
|
+
/** Account-variance profiles for the default seed. */
|
|
231
|
+
export const FortnoxEmulatorProfile = Schema.Literals([
|
|
232
|
+
'default',
|
|
233
|
+
'empty-company',
|
|
234
|
+
'no-booked-invoices'
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
export type FortnoxEmulatorProfile = typeof FortnoxEmulatorProfile.Type
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A typed seed. Start from `profile` (default `'default'`, the fixture entities); `company`,
|
|
241
|
+
* `customers`, and `invoices`, when given, replace that part of the profile.
|
|
242
|
+
*/
|
|
243
|
+
export const FortnoxEmulatorSeed = Schema.Struct({
|
|
244
|
+
profile: Schema.optionalKey(FortnoxEmulatorProfile),
|
|
245
|
+
company: Schema.optionalKey(FortnoxEmulatorCompany),
|
|
246
|
+
customers: Schema.optionalKey(Schema.Array(FortnoxEmulatorCustomerSeed)),
|
|
247
|
+
invoices: Schema.optionalKey(Schema.Array(FortnoxEmulatorInvoiceSeed))
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
export type FortnoxEmulatorSeed = typeof FortnoxEmulatorSeed.Type
|
|
251
|
+
|
|
252
|
+
const strict = { onExcessProperty: 'error' } as const
|
|
253
|
+
|
|
254
|
+
const decodeSeedInput = Schema.decodeUnknownResult(FortnoxEmulatorSeed, strict)
|
|
255
|
+
|
|
256
|
+
const decodeStateInput = Schema.decodeUnknownResult(FortnoxEmulatorStateSchema, strict)
|
|
257
|
+
|
|
258
|
+
const issueMessage = (issue: Schema.SchemaError['issue']): string =>
|
|
259
|
+
new Schema.SchemaError(issue).message
|
|
260
|
+
|
|
261
|
+
/** `CountryCode` to the Swedish country name Fortnox derives `Country` from (a small table). */
|
|
262
|
+
export const fortnoxEmulatorCountries = new Map([
|
|
263
|
+
['SE', 'Sverige'],
|
|
264
|
+
['NO', 'Norge'],
|
|
265
|
+
['DK', 'Danmark'],
|
|
266
|
+
['FI', 'Finland'],
|
|
267
|
+
['DE', 'Tyskland'],
|
|
268
|
+
['GB', 'Storbritannien'],
|
|
269
|
+
['US', 'USA']
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
/** VAT percent to Fortnox sales VAT code. */
|
|
273
|
+
export const fortnoxEmulatorVatCodes = new Map([
|
|
274
|
+
[25, 'MP1'],
|
|
275
|
+
[12, 'MP2'],
|
|
276
|
+
[6, 'MP3'],
|
|
277
|
+
[0, 'MF']
|
|
278
|
+
])
|
|
279
|
+
|
|
280
|
+
export const round2 = (value: number): number => Math.round(value * 100) / 100
|
|
281
|
+
|
|
282
|
+
/** Row total: `Price × DeliveredQuantity`, less a percent (or amount) discount. */
|
|
283
|
+
export const rowTotal = (
|
|
284
|
+
row: Pick<FortnoxEmulatorInvoiceRow, 'Price' | 'DeliveredQuantity' | 'Discount' | 'DiscountType'>
|
|
285
|
+
): number => {
|
|
286
|
+
const gross = row.Price * Number(row.DeliveredQuantity)
|
|
287
|
+
|
|
288
|
+
return round2(
|
|
289
|
+
row.DiscountType === 'AMOUNT' ? gross - row.Discount : (gross * (100 - row.Discount)) / 100
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
type RowInput = Pick<
|
|
294
|
+
FortnoxEmulatorInvoiceRow,
|
|
295
|
+
| 'RowId'
|
|
296
|
+
| 'AccountNumber'
|
|
297
|
+
| 'ArticleNumber'
|
|
298
|
+
| 'CostCenter'
|
|
299
|
+
| 'DeliveredQuantity'
|
|
300
|
+
| 'Description'
|
|
301
|
+
| 'Discount'
|
|
302
|
+
| 'DiscountType'
|
|
303
|
+
| 'Price'
|
|
304
|
+
| 'Project'
|
|
305
|
+
| 'Unit'
|
|
306
|
+
| 'VAT'
|
|
307
|
+
>
|
|
308
|
+
|
|
309
|
+
/** A full row (fixture key order) with derived totals and VAT code. */
|
|
310
|
+
export const buildRow = (input: RowInput): FortnoxEmulatorInvoiceRow => {
|
|
311
|
+
const total = rowTotal(input)
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
AccountNumber: input.AccountNumber,
|
|
315
|
+
ArticleNumber: input.ArticleNumber,
|
|
316
|
+
ContributionPercent: '0',
|
|
317
|
+
ContributionValue: '0',
|
|
318
|
+
CostCenter: input.CostCenter,
|
|
319
|
+
DeliveredQuantity: input.DeliveredQuantity,
|
|
320
|
+
Description: input.Description,
|
|
321
|
+
Discount: input.Discount,
|
|
322
|
+
DiscountType: input.DiscountType,
|
|
323
|
+
HouseWork: false,
|
|
324
|
+
Price: input.Price,
|
|
325
|
+
PriceExcludingVAT: input.Price,
|
|
326
|
+
Project: input.Project,
|
|
327
|
+
RowId: input.RowId,
|
|
328
|
+
Total: total,
|
|
329
|
+
TotalExcludingVAT: total,
|
|
330
|
+
Unit: input.Unit,
|
|
331
|
+
VAT: input.VAT,
|
|
332
|
+
VATCode: fortnoxEmulatorVatCodes.get(input.VAT) ?? ''
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Defaults for a new row (no positional match). */
|
|
337
|
+
export const newRowDefaults: Omit<RowInput, 'RowId'> = {
|
|
338
|
+
AccountNumber: 3001,
|
|
339
|
+
ArticleNumber: '',
|
|
340
|
+
CostCenter: '',
|
|
341
|
+
DeliveredQuantity: '1.00',
|
|
342
|
+
Description: '',
|
|
343
|
+
Discount: 0,
|
|
344
|
+
DiscountType: 'PERCENT',
|
|
345
|
+
Price: 0,
|
|
346
|
+
Project: '',
|
|
347
|
+
Unit: '',
|
|
348
|
+
VAT: 25
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Fortnox-style quantity: two decimals (`"2.00"`). */
|
|
352
|
+
export const formatQuantity = (value: number): string => value.toFixed(2)
|
|
353
|
+
|
|
354
|
+
export type InvoiceTotals = Pick<
|
|
355
|
+
FortnoxEmulatorInvoice,
|
|
356
|
+
'Net' | 'Gross' | 'TotalVAT' | 'Total' | 'TotalToPay'
|
|
357
|
+
>
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Invoice totals from the rows: `Net = Gross = Σ row totals`, `TotalVAT = Σ row total × VAT%`,
|
|
361
|
+
* and `Total = TotalToPay = Net + TotalVAT` rounded to whole kronor (half up), matching the
|
|
362
|
+
* synthetic row-discount fixture (1150 + 287.5 → 1438).
|
|
363
|
+
*/
|
|
364
|
+
export const invoiceTotals = (rows: ReadonlyArray<FortnoxEmulatorInvoiceRow>): InvoiceTotals => {
|
|
365
|
+
const net = round2(rows.reduce((sum, row) => sum + row.Total, 0))
|
|
366
|
+
const vat = round2(rows.reduce((sum, row) => sum + (row.Total * row.VAT) / 100, 0))
|
|
367
|
+
const total = Math.round(net + vat)
|
|
368
|
+
|
|
369
|
+
return { Net: net, Gross: net, TotalVAT: vat, Total: total, TotalToPay: total }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Default entities. Fixture-derived: customer 1001 (empty-string fixture) and the invoice headers
|
|
373
|
+
// of 101-105 (list, payment-filter, row-discount, and email fixtures). Where two fixtures disagree
|
|
374
|
+
// the more specific one wins: invoice 103 (and its two rows) follows the row-discount fixture, not
|
|
375
|
+
// the list fixture (its Balance, DueDate, InvoiceDate, and OCR differ there). Synthesized: customer
|
|
376
|
+
// 1002, the rows of invoices 101, 102, 104, and 105 (chosen so their computed totals equal the
|
|
377
|
+
// fixture totals), the company information (no fixture), and every default `customerFromSeed` and
|
|
378
|
+
// `invoiceFromSeed` fill in for a field the seed omits.
|
|
379
|
+
|
|
380
|
+
const exampleCustomer: FortnoxEmulatorCustomerSeed = {
|
|
381
|
+
CustomerNumber: '1001',
|
|
382
|
+
Name: 'Example Customer AB',
|
|
383
|
+
Active: true,
|
|
384
|
+
Address1: 'Exempelgatan 1',
|
|
385
|
+
Address2: '',
|
|
386
|
+
City: 'Exempelstad',
|
|
387
|
+
Comments: 'Synthetic note: invoice monthly by email.',
|
|
388
|
+
CostCenter: '',
|
|
389
|
+
CountryCode: 'SE',
|
|
390
|
+
Currency: 'SEK',
|
|
391
|
+
Email: 'billing@example.test',
|
|
392
|
+
EmailInvoice: 'billing@example.test',
|
|
393
|
+
OrganisationNumber: '000000-0000',
|
|
394
|
+
OurReference: '',
|
|
395
|
+
Phone1: '',
|
|
396
|
+
Phone2: '',
|
|
397
|
+
TermsOfPayment: '30',
|
|
398
|
+
Type: 'COMPANY',
|
|
399
|
+
VATNumber: '',
|
|
400
|
+
VATType: 'SEVAT',
|
|
401
|
+
YourReference: '',
|
|
402
|
+
ZipCode: '000 00'
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const exampleTrading: FortnoxEmulatorCustomerSeed = {
|
|
406
|
+
CustomerNumber: '1002',
|
|
407
|
+
Name: 'Example Trading AB',
|
|
408
|
+
Active: true,
|
|
409
|
+
Address1: 'Exempelvägen 2',
|
|
410
|
+
City: 'Exempelstad',
|
|
411
|
+
Comments: '',
|
|
412
|
+
CountryCode: 'SE',
|
|
413
|
+
Currency: 'SEK',
|
|
414
|
+
Email: 'accounts@example.test',
|
|
415
|
+
EmailInvoice: 'accounts@example.test',
|
|
416
|
+
OrganisationNumber: '000000-0001',
|
|
417
|
+
TermsOfPayment: '30',
|
|
418
|
+
Type: 'COMPANY',
|
|
419
|
+
VATType: 'SEVAT',
|
|
420
|
+
ZipCode: '000 01'
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const singleRow = (description: string, price: number): FortnoxEmulatorInvoiceRowSeed => ({
|
|
424
|
+
AccountNumber: 3001,
|
|
425
|
+
Description: description,
|
|
426
|
+
DeliveredQuantity: '1.00',
|
|
427
|
+
Price: price,
|
|
428
|
+
VAT: 25,
|
|
429
|
+
Discount: 0,
|
|
430
|
+
DiscountType: 'PERCENT'
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
const defaultInvoices: ReadonlyArray<FortnoxEmulatorInvoiceSeed> = [
|
|
434
|
+
{
|
|
435
|
+
DocumentNumber: '101',
|
|
436
|
+
CustomerNumber: '1001',
|
|
437
|
+
Balance: 0,
|
|
438
|
+
Booked: true,
|
|
439
|
+
DueDate: '2026-09-30',
|
|
440
|
+
FinalPayDate: '2026-09-20',
|
|
441
|
+
InvoiceDate: '2026-08-31',
|
|
442
|
+
OCR: '10151',
|
|
443
|
+
Sent: true,
|
|
444
|
+
VoucherNumber: 12,
|
|
445
|
+
VoucherSeries: 'B',
|
|
446
|
+
VoucherYear: 3,
|
|
447
|
+
InvoiceRows: [singleRow('Monthly service (synthetic)', 1000)]
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
DocumentNumber: '102',
|
|
451
|
+
CustomerNumber: '1002',
|
|
452
|
+
Balance: 3750,
|
|
453
|
+
Booked: true,
|
|
454
|
+
DueDate: '2026-09-30',
|
|
455
|
+
InvoiceDate: '2026-08-31',
|
|
456
|
+
OCR: '10250',
|
|
457
|
+
Sent: true,
|
|
458
|
+
VoucherNumber: 13,
|
|
459
|
+
VoucherSeries: 'B',
|
|
460
|
+
VoucherYear: 3,
|
|
461
|
+
InvoiceRows: [singleRow('Project delivery (synthetic)', 3000)]
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
DocumentNumber: '103',
|
|
465
|
+
CustomerNumber: '1001',
|
|
466
|
+
Booked: false,
|
|
467
|
+
DueDate: '2026-10-31',
|
|
468
|
+
InvoiceDate: '2026-09-30',
|
|
469
|
+
OCR: '10350',
|
|
470
|
+
InvoiceRows: [
|
|
471
|
+
{
|
|
472
|
+
RowId: 1,
|
|
473
|
+
AccountNumber: 3001,
|
|
474
|
+
Description: 'Consulting hours (synthetic)',
|
|
475
|
+
DeliveredQuantity: '2.00',
|
|
476
|
+
Unit: 'h',
|
|
477
|
+
Price: 500,
|
|
478
|
+
VAT: 25,
|
|
479
|
+
Discount: 5,
|
|
480
|
+
DiscountType: 'PERCENT'
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
RowId: 2,
|
|
484
|
+
AccountNumber: 3001,
|
|
485
|
+
Description: 'Travel (synthetic)',
|
|
486
|
+
DeliveredQuantity: '1.00',
|
|
487
|
+
Price: 250,
|
|
488
|
+
VAT: 25,
|
|
489
|
+
Discount: 0,
|
|
490
|
+
DiscountType: 'PERCENT'
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
DocumentNumber: '104',
|
|
496
|
+
CustomerNumber: '1001',
|
|
497
|
+
Balance: 625,
|
|
498
|
+
Booked: true,
|
|
499
|
+
DueDate: '2026-10-31',
|
|
500
|
+
InvoiceDate: '2026-09-30',
|
|
501
|
+
VoucherNumber: 14,
|
|
502
|
+
VoucherSeries: 'B',
|
|
503
|
+
VoucherYear: 3,
|
|
504
|
+
EmailInformation: {
|
|
505
|
+
EmailAddressFrom: 'invoices@example.test',
|
|
506
|
+
EmailAddressTo: 'billing@example.test',
|
|
507
|
+
EmailAddressCC: '',
|
|
508
|
+
EmailAddressBCC: '',
|
|
509
|
+
EmailSubject: 'Invoice {no} from Example AB',
|
|
510
|
+
EmailBody: 'Synthetic invoice email body.'
|
|
511
|
+
},
|
|
512
|
+
InvoiceRows: [singleRow('Support hours (synthetic)', 500)]
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
DocumentNumber: '105',
|
|
516
|
+
CustomerNumber: '1001',
|
|
517
|
+
Booked: false,
|
|
518
|
+
DueDate: '2026-09-15',
|
|
519
|
+
InvoiceDate: '2026-08-16',
|
|
520
|
+
InvoiceRows: [singleRow('Workshop (synthetic)', 1000)]
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
const defaultCompany: FortnoxEmulatorCompany = {
|
|
525
|
+
CompanyName: 'Example AB',
|
|
526
|
+
OrganizationNumber: '000000-0002',
|
|
527
|
+
DatabaseNumber: 100001,
|
|
528
|
+
Address: 'Exempelgatan 10',
|
|
529
|
+
City: 'Exempelstad',
|
|
530
|
+
ZipCode: '000 02',
|
|
531
|
+
CountryCode: 'SE',
|
|
532
|
+
VisitAddress: 'Exempelgatan 10',
|
|
533
|
+
VisitCity: 'Exempelstad',
|
|
534
|
+
VisitZipCode: '000 02',
|
|
535
|
+
VisitCountryCode: 'SE'
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
type ProfileEntities = {
|
|
539
|
+
readonly company: FortnoxEmulatorCompany
|
|
540
|
+
readonly customers: ReadonlyArray<FortnoxEmulatorCustomerSeed>
|
|
541
|
+
readonly invoices: ReadonlyArray<FortnoxEmulatorInvoiceSeed>
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const unbooked = (invoice: FortnoxEmulatorInvoiceSeed): FortnoxEmulatorInvoiceSeed => {
|
|
545
|
+
const { Balance: _balance, FinalPayDate: _finalPayDate, ...rest } = invoice
|
|
546
|
+
|
|
547
|
+
return { ...rest, Booked: false, VoucherNumber: null, VoucherSeries: null, VoucherYear: null }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const profileEntities = (profile: FortnoxEmulatorProfile): ProfileEntities => {
|
|
551
|
+
switch (profile) {
|
|
552
|
+
case 'default':
|
|
553
|
+
return {
|
|
554
|
+
company: defaultCompany,
|
|
555
|
+
customers: [exampleCustomer, exampleTrading],
|
|
556
|
+
invoices: defaultInvoices
|
|
557
|
+
}
|
|
558
|
+
case 'empty-company':
|
|
559
|
+
return { company: defaultCompany, customers: [], invoices: [] }
|
|
560
|
+
case 'no-booked-invoices':
|
|
561
|
+
return {
|
|
562
|
+
company: defaultCompany,
|
|
563
|
+
customers: [exampleCustomer, exampleTrading],
|
|
564
|
+
invoices: defaultInvoices.map(unbooked)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Numeric-aware order for Fortnox numbers (`"999" < "1001"`). */
|
|
570
|
+
export const compareNumbers = (left: string, right: string): number =>
|
|
571
|
+
left.localeCompare(right, 'en', { numeric: true })
|
|
572
|
+
|
|
573
|
+
const customerFromSeed = (seed: FortnoxEmulatorCustomerSeed): FortnoxEmulatorCustomer => {
|
|
574
|
+
const countryCode = seed.CountryCode ?? 'SE'
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
CustomerNumber: seed.CustomerNumber,
|
|
578
|
+
Name: seed.Name,
|
|
579
|
+
Active: seed.Active ?? true,
|
|
580
|
+
Address1: seed.Address1 ?? '',
|
|
581
|
+
Address2: seed.Address2 ?? '',
|
|
582
|
+
City: seed.City ?? '',
|
|
583
|
+
Comments: seed.Comments ?? '',
|
|
584
|
+
CostCenter: seed.CostCenter ?? '',
|
|
585
|
+
Country: seed.Country ?? fortnoxEmulatorCountries.get(countryCode) ?? '',
|
|
586
|
+
CountryCode: countryCode,
|
|
587
|
+
Currency: seed.Currency ?? 'SEK',
|
|
588
|
+
Email: seed.Email ?? '',
|
|
589
|
+
EmailInvoice: seed.EmailInvoice ?? '',
|
|
590
|
+
OrganisationNumber: seed.OrganisationNumber ?? '',
|
|
591
|
+
OurReference: seed.OurReference ?? '',
|
|
592
|
+
Phone1: seed.Phone1 ?? '',
|
|
593
|
+
Phone2: seed.Phone2 ?? '',
|
|
594
|
+
TermsOfPayment: seed.TermsOfPayment ?? '30',
|
|
595
|
+
Type: seed.Type ?? 'COMPANY',
|
|
596
|
+
VATNumber: seed.VATNumber ?? '',
|
|
597
|
+
VATType: seed.VATType ?? 'SEVAT',
|
|
598
|
+
YourReference: seed.YourReference ?? '',
|
|
599
|
+
ZipCode: seed.ZipCode ?? ''
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Default invoice email for a customer (the email fixture's sender, subject, and body). */
|
|
604
|
+
export const defaultEmailInformation = (
|
|
605
|
+
customer: FortnoxEmulatorCustomer | undefined,
|
|
606
|
+
company: FortnoxEmulatorCompany
|
|
607
|
+
): FortnoxEmulatorInvoice['EmailInformation'] => ({
|
|
608
|
+
EmailAddressFrom: 'invoices@example.test',
|
|
609
|
+
EmailAddressTo: customer?.EmailInvoice ?? '',
|
|
610
|
+
EmailAddressCC: '',
|
|
611
|
+
EmailAddressBCC: '',
|
|
612
|
+
EmailSubject: `Invoice {no} from ${company.CompanyName}`,
|
|
613
|
+
EmailBody: 'Synthetic invoice email body.'
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
export type RowIdSource = { next: number }
|
|
617
|
+
|
|
618
|
+
const rowFromSeed = (
|
|
619
|
+
seed: FortnoxEmulatorInvoiceRowSeed,
|
|
620
|
+
ids: RowIdSource
|
|
621
|
+
): FortnoxEmulatorInvoiceRow =>
|
|
622
|
+
buildRow({
|
|
623
|
+
...newRowDefaults,
|
|
624
|
+
...seed,
|
|
625
|
+
DeliveredQuantity: formatQuantity(Number(seed.DeliveredQuantity ?? '1')),
|
|
626
|
+
RowId: seed.RowId ?? ids.next++
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
/** Build a stored invoice from a seed; row ids come from `ids` unless the seed names them. */
|
|
630
|
+
export const invoiceFromSeed = (
|
|
631
|
+
seed: FortnoxEmulatorInvoiceSeed,
|
|
632
|
+
customer: FortnoxEmulatorCustomer | undefined,
|
|
633
|
+
company: FortnoxEmulatorCompany,
|
|
634
|
+
ids: RowIdSource
|
|
635
|
+
): FortnoxEmulatorInvoice => {
|
|
636
|
+
const rows = (seed.InvoiceRows ?? []).map(row => rowFromSeed(row, ids))
|
|
637
|
+
const totals = invoiceTotals(rows)
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
DocumentNumber: seed.DocumentNumber,
|
|
641
|
+
CustomerNumber: seed.CustomerNumber,
|
|
642
|
+
CustomerName: seed.CustomerName ?? customer?.Name ?? '',
|
|
643
|
+
Balance: seed.Balance ?? totals.Total,
|
|
644
|
+
Booked: seed.Booked ?? false,
|
|
645
|
+
Cancelled: seed.Cancelled ?? false,
|
|
646
|
+
Comments: seed.Comments ?? '',
|
|
647
|
+
CostCenter: seed.CostCenter ?? '',
|
|
648
|
+
Credit: seed.Credit ?? 'false',
|
|
649
|
+
Currency: seed.Currency ?? customer?.Currency ?? 'SEK',
|
|
650
|
+
CurrencyRate: seed.CurrencyRate ?? 1,
|
|
651
|
+
CurrencyUnit: seed.CurrencyUnit ?? 1,
|
|
652
|
+
DueDate: seed.DueDate ?? null,
|
|
653
|
+
EmailInformation: seed.EmailInformation ?? defaultEmailInformation(customer, company),
|
|
654
|
+
ExternalInvoiceReference1: seed.ExternalInvoiceReference1 ?? '',
|
|
655
|
+
ExternalInvoiceReference2: seed.ExternalInvoiceReference2 ?? '',
|
|
656
|
+
FinalPayDate: seed.FinalPayDate ?? null,
|
|
657
|
+
Gross: totals.Gross,
|
|
658
|
+
InvoiceDate: seed.InvoiceDate ?? '2026-09-30',
|
|
659
|
+
InvoiceRows: rows,
|
|
660
|
+
InvoiceType: seed.InvoiceType ?? 'INVOICE',
|
|
661
|
+
Net: totals.Net,
|
|
662
|
+
NotCompleted: seed.NotCompleted ?? false,
|
|
663
|
+
NoxFinans: seed.NoxFinans ?? false,
|
|
664
|
+
OCR: seed.OCR ?? '',
|
|
665
|
+
OurReference: seed.OurReference ?? '',
|
|
666
|
+
Project: seed.Project ?? '',
|
|
667
|
+
Sent: seed.Sent ?? false,
|
|
668
|
+
TermsOfPayment: seed.TermsOfPayment ?? customer?.TermsOfPayment ?? '30',
|
|
669
|
+
Total: totals.Total,
|
|
670
|
+
TotalToPay: totals.TotalToPay,
|
|
671
|
+
TotalVAT: totals.TotalVAT,
|
|
672
|
+
VoucherNumber: seed.VoucherNumber ?? null,
|
|
673
|
+
VoucherSeries: seed.VoucherSeries ?? null,
|
|
674
|
+
VoucherYear: seed.VoucherYear ?? null,
|
|
675
|
+
WayOfDelivery: seed.WayOfDelivery ?? '',
|
|
676
|
+
YourReference: seed.YourReference ?? ''
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const duplicate = (values: ReadonlyArray<string | number>): string | number | undefined =>
|
|
681
|
+
values.find((value, index) => values.indexOf(value) !== index)
|
|
682
|
+
|
|
683
|
+
const quantityProblem = (rows: ReadonlyArray<FortnoxEmulatorInvoiceRowSeed>) =>
|
|
684
|
+
rows.find(
|
|
685
|
+
row => row.DeliveredQuantity !== undefined && !Number.isFinite(Number(row.DeliveredQuantity))
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
/** Integrity problems a decoded seed can still have (duplicates, dangling references). */
|
|
689
|
+
const seedProblem = (entities: ProfileEntities): string | undefined => {
|
|
690
|
+
const customerNumbers = entities.customers.map(customer => customer.CustomerNumber)
|
|
691
|
+
const documentNumbers = entities.invoices.map(invoice => invoice.DocumentNumber)
|
|
692
|
+
|
|
693
|
+
const rowIds = entities.invoices.flatMap(invoice =>
|
|
694
|
+
(invoice.InvoiceRows ?? []).flatMap(row => (row.RowId === undefined ? [] : [row.RowId]))
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
const duplicateCustomer = duplicate(customerNumbers)
|
|
698
|
+
const duplicateInvoice = duplicate(documentNumbers)
|
|
699
|
+
const duplicateRow = duplicate(rowIds)
|
|
700
|
+
|
|
701
|
+
if (duplicateCustomer !== undefined) return `duplicate CustomerNumber ${duplicateCustomer}`
|
|
702
|
+
|
|
703
|
+
if (duplicateInvoice !== undefined) return `duplicate DocumentNumber ${duplicateInvoice}`
|
|
704
|
+
|
|
705
|
+
if (duplicateRow !== undefined) return `duplicate RowId ${duplicateRow}`
|
|
706
|
+
|
|
707
|
+
const dangling = entities.invoices.find(
|
|
708
|
+
invoice => !customerNumbers.includes(invoice.CustomerNumber)
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
if (dangling !== undefined) {
|
|
712
|
+
return `invoice ${dangling.DocumentNumber} references missing customer ${dangling.CustomerNumber}`
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const badQuantity = entities.invoices.find(
|
|
716
|
+
invoice => quantityProblem(invoice.InvoiceRows ?? []) !== undefined
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
return badQuantity === undefined
|
|
720
|
+
? undefined
|
|
721
|
+
: `invoice ${badQuantity.DocumentNumber} has a non-numeric DeliveredQuantity`
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const nextNumber = (numbers: ReadonlyArray<string>): number =>
|
|
725
|
+
numbers.reduce((max, value) => {
|
|
726
|
+
const parsed = Number(value)
|
|
727
|
+
|
|
728
|
+
return Number.isSafeInteger(parsed) && parsed >= max ? parsed + 1 : max
|
|
729
|
+
}, 1)
|
|
730
|
+
|
|
731
|
+
/** Build the emulator state for a decoded seed; a string is an integrity problem. */
|
|
732
|
+
const stateFromSeed = (seed: FortnoxEmulatorSeed): FortnoxEmulatorState | string => {
|
|
733
|
+
const profile = profileEntities(seed.profile ?? 'default')
|
|
734
|
+
|
|
735
|
+
const entities: ProfileEntities = {
|
|
736
|
+
company: seed.company ?? profile.company,
|
|
737
|
+
customers: seed.customers ?? profile.customers,
|
|
738
|
+
invoices: seed.invoices ?? profile.invoices
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const problem = seedProblem(entities)
|
|
742
|
+
|
|
743
|
+
if (problem !== undefined) {
|
|
744
|
+
return problem
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const customers = entities.customers
|
|
748
|
+
.map(customerFromSeed)
|
|
749
|
+
.sort((left, right) => compareNumbers(left.CustomerNumber, right.CustomerNumber))
|
|
750
|
+
|
|
751
|
+
const explicitRowIds = entities.invoices.flatMap(invoice =>
|
|
752
|
+
(invoice.InvoiceRows ?? []).flatMap(row => (row.RowId === undefined ? [] : [row.RowId]))
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
const ids: RowIdSource = { next: Math.max(0, ...explicitRowIds) + 1 }
|
|
756
|
+
|
|
757
|
+
const invoices = entities.invoices
|
|
758
|
+
.map(invoice =>
|
|
759
|
+
invoiceFromSeed(
|
|
760
|
+
invoice,
|
|
761
|
+
customers.find(customer => customer.CustomerNumber === invoice.CustomerNumber),
|
|
762
|
+
entities.company,
|
|
763
|
+
ids
|
|
764
|
+
)
|
|
765
|
+
)
|
|
766
|
+
.sort((left, right) => compareNumbers(left.DocumentNumber, right.DocumentNumber))
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
company: entities.company,
|
|
770
|
+
customers,
|
|
771
|
+
invoices,
|
|
772
|
+
outbox: [],
|
|
773
|
+
counters: {
|
|
774
|
+
nextRowId: ids.next,
|
|
775
|
+
nextDocumentNumber: nextNumber(invoices.map(invoice => invoice.DocumentNumber))
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** Decode and build a seed; a string is the reason it is invalid. */
|
|
781
|
+
export const buildSeedState = (input: unknown): FortnoxEmulatorState | string => {
|
|
782
|
+
const decoded = decodeSeedInput(input)
|
|
783
|
+
|
|
784
|
+
return Result.isFailure(decoded)
|
|
785
|
+
? issueMessage(decoded.failure.issue)
|
|
786
|
+
: stateFromSeed(decoded.success)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Decode a full state (a restored snapshot); a string is the reason it is invalid. */
|
|
790
|
+
export const decodeState = (input: unknown): FortnoxEmulatorState | string => {
|
|
791
|
+
const decoded = decodeStateInput(input)
|
|
792
|
+
|
|
793
|
+
if (Result.isFailure(decoded)) {
|
|
794
|
+
return issueMessage(decoded.failure.issue)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return { ...decoded.success }
|
|
798
|
+
}
|