@stamhoofd/models 2.120.6 → 2.122.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.
Files changed (296) hide show
  1. package/dist/factories/GroupFactory.d.ts.map +1 -1
  2. package/dist/factories/GroupFactory.js +1 -1
  3. package/dist/factories/GroupFactory.js.map +1 -1
  4. package/dist/factories/OrganizationFactory.d.ts +2 -1
  5. package/dist/factories/OrganizationFactory.d.ts.map +1 -1
  6. package/dist/factories/OrganizationFactory.js +9 -1
  7. package/dist/factories/OrganizationFactory.js.map +1 -1
  8. package/dist/factories/RegistrationInvitationFactory.d.ts +15 -0
  9. package/dist/factories/RegistrationInvitationFactory.d.ts.map +1 -0
  10. package/dist/factories/RegistrationInvitationFactory.js +18 -0
  11. package/dist/factories/RegistrationInvitationFactory.js.map +1 -0
  12. package/dist/factories/STPackageFactory.js.map +1 -1
  13. package/dist/factories/UserFactory.d.ts.map +1 -1
  14. package/dist/factories/UserFactory.js +2 -2
  15. package/dist/factories/UserFactory.js.map +1 -1
  16. package/dist/factories/index.d.ts +1 -0
  17. package/dist/factories/index.d.ts.map +1 -1
  18. package/dist/factories/index.js +1 -0
  19. package/dist/factories/index.js.map +1 -1
  20. package/dist/helpers/EmailBuilder.d.ts.map +1 -1
  21. package/dist/helpers/EmailBuilder.js +8 -8
  22. package/dist/helpers/EmailBuilder.js.map +1 -1
  23. package/dist/helpers/Handlebars.d.ts.map +1 -1
  24. package/dist/helpers/Handlebars.js +10 -1
  25. package/dist/helpers/Handlebars.js.map +1 -1
  26. package/dist/helpers/InvoiceCounter.d.ts +24 -0
  27. package/dist/helpers/InvoiceCounter.d.ts.map +1 -0
  28. package/dist/helpers/InvoiceCounter.js +133 -0
  29. package/dist/helpers/InvoiceCounter.js.map +1 -0
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +0 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/migrations/1605262045-import-postcodes.d.ts.map +1 -1
  35. package/dist/migrations/1605262045-import-postcodes.js +58 -24
  36. package/dist/migrations/1605262045-import-postcodes.js.map +1 -1
  37. package/dist/migrations/1605262046-import-postcodes-nl.d.ts.map +1 -1
  38. package/dist/migrations/1605262046-import-postcodes-nl.js +54 -17
  39. package/dist/migrations/1605262046-import-postcodes-nl.js.map +1 -1
  40. package/dist/migrations/1719567881-organization-periodId.sql +2 -0
  41. package/dist/migrations/1719567882-groups-periodId.sql +2 -0
  42. package/dist/migrations/1720080975-convert-charset.d.ts +4 -0
  43. package/dist/migrations/1720080975-convert-charset.d.ts.map +1 -0
  44. package/dist/migrations/1720080975-convert-charset.js +26 -0
  45. package/dist/migrations/1720080975-convert-charset.js.map +1 -0
  46. package/dist/migrations/1720080976-convert-charset-leads.d.ts.map +1 -1
  47. package/dist/migrations/1720080976-convert-charset-leads.js +11 -10
  48. package/dist/migrations/1720080976-convert-charset-leads.js.map +1 -1
  49. package/dist/migrations/1721400546-users-memberId.sql +2 -0
  50. package/dist/migrations/1722269236-group-waitinglist-id.sql +2 -1
  51. package/dist/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  52. package/dist/migrations/1722525787-depending-balance-item.sql +2 -0
  53. package/dist/migrations/1722963554-registration-group-price-and-options.sql +1 -1
  54. package/dist/migrations/1723652797-payments-paying-organization-id-fk.sql +2 -0
  55. package/dist/migrations/1733317908-added-missing-organization-fk-on-registrations.sql +2 -0
  56. package/dist/migrations/1733317910-paying-organization-id-fk.sql +2 -0
  57. package/dist/migrations/1733504881-negative-invoice-id.sql +6 -0
  58. package/dist/migrations/1733994455-balance-item-status-open.d.ts +4 -0
  59. package/dist/migrations/1733994455-balance-item-status-open.d.ts.map +1 -0
  60. package/dist/migrations/1733994455-balance-item-status-open.js +28 -0
  61. package/dist/migrations/1733994455-balance-item-status-open.js.map +1 -0
  62. package/dist/migrations/1763216320-bigint-balance-item-payments.sql +2 -0
  63. package/dist/migrations/1763216320-bigint-balance-items.sql +5 -0
  64. package/dist/migrations/1763216320-bigint-orders.sql +2 -0
  65. package/dist/migrations/1763216320-bigint-payments.sql +2 -0
  66. package/dist/migrations/1763216332-bigint-balance-item-price-total.sql +2 -0
  67. package/dist/migrations/1769087808-corrected-invoice-user-agent.sql +2 -0
  68. package/dist/migrations/1769087809-payments-invoice-id.sql +2 -0
  69. package/dist/migrations/1772033555-balance-item-package-id.sql +2 -0
  70. package/dist/migrations/1776873089-create-registration-invitations-table.sql +13 -0
  71. package/dist/migrations/1778657958-payments-create-mandate.sql +2 -0
  72. package/dist/migrations/1778657959-payments-mandate-id.sql +2 -0
  73. package/dist/migrations/1778796615-payments-reversing-payment-id.sql +5 -0
  74. package/dist/migrations/1778950642-price-invoiced.sql +2 -0
  75. package/dist/migrations/1779443446-transfer-fees.sql +3 -0
  76. package/dist/migrations/1779709174-used-register-code-balance-item-id.sql +5 -0
  77. package/dist/migrations/1779968328-payments-admin-user-id.sql +5 -0
  78. package/dist/migrations/1779970611-payments-refunded-amount.sql +2 -0
  79. package/dist/migrations/1779972640-balance-items-failed-at.sql +2 -0
  80. package/dist/migrations/1780328285-document-template-locked.sql +2 -0
  81. package/dist/migrations/1780328286-document-locked.sql +2 -0
  82. package/dist/migrations/1780412083-documents-set-locked.d.ts +4 -0
  83. package/dist/migrations/1780412083-documents-set-locked.d.ts.map +1 -0
  84. package/dist/migrations/1780412083-documents-set-locked.js +14 -0
  85. package/dist/migrations/1780412083-documents-set-locked.js.map +1 -0
  86. package/dist/migrations/1780928401-v1-groups-migration-data.d.ts +4 -0
  87. package/dist/migrations/1780928401-v1-groups-migration-data.d.ts.map +1 -0
  88. package/dist/migrations/1780928401-v1-groups-migration-data.js +44 -0
  89. package/dist/migrations/1780928401-v1-groups-migration-data.js.map +1 -0
  90. package/dist/models/BalanceItem.d.ts +14 -1
  91. package/dist/models/BalanceItem.d.ts.map +1 -1
  92. package/dist/models/BalanceItem.js +91 -41
  93. package/dist/models/BalanceItem.js.map +1 -1
  94. package/dist/models/CachedBalance.d.ts +6 -1
  95. package/dist/models/CachedBalance.d.ts.map +1 -1
  96. package/dist/models/CachedBalance.js +3 -2
  97. package/dist/models/CachedBalance.js.map +1 -1
  98. package/dist/models/Document.d.ts +4 -0
  99. package/dist/models/Document.d.ts.map +1 -1
  100. package/dist/models/Document.js +26 -3
  101. package/dist/models/Document.js.map +1 -1
  102. package/dist/models/DocumentTemplate.d.ts +4 -0
  103. package/dist/models/DocumentTemplate.d.ts.map +1 -1
  104. package/dist/models/DocumentTemplate.js +37 -1
  105. package/dist/models/DocumentTemplate.js.map +1 -1
  106. package/dist/models/Email.d.ts.map +1 -1
  107. package/dist/models/Email.js +1 -1
  108. package/dist/models/Email.js.map +1 -1
  109. package/dist/models/EmailVerificationCode.d.ts.map +1 -1
  110. package/dist/models/EmailVerificationCode.js +3 -13
  111. package/dist/models/EmailVerificationCode.js.map +1 -1
  112. package/dist/models/Event.d.ts +2 -1
  113. package/dist/models/Event.d.ts.map +1 -1
  114. package/dist/models/Event.js +3 -0
  115. package/dist/models/Event.js.map +1 -1
  116. package/dist/models/EventNotification.d.ts.map +1 -1
  117. package/dist/models/EventNotification.js +5 -5
  118. package/dist/models/EventNotification.js.map +1 -1
  119. package/dist/models/Group.d.ts +4 -0
  120. package/dist/models/Group.d.ts.map +1 -1
  121. package/dist/models/Group.js +17 -0
  122. package/dist/models/Group.js.map +1 -1
  123. package/dist/models/Invoice.d.ts +1 -0
  124. package/dist/models/Invoice.d.ts.map +1 -1
  125. package/dist/models/Invoice.js +8 -8
  126. package/dist/models/Invoice.js.map +1 -1
  127. package/dist/models/MemberPlatformMembership.d.ts.map +1 -1
  128. package/dist/models/MemberPlatformMembership.js +9 -0
  129. package/dist/models/MemberPlatformMembership.js.map +1 -1
  130. package/dist/models/MollieToken.d.ts +4 -8
  131. package/dist/models/MollieToken.d.ts.map +1 -1
  132. package/dist/models/MollieToken.js +37 -90
  133. package/dist/models/MollieToken.js.map +1 -1
  134. package/dist/models/Order.d.ts.map +1 -1
  135. package/dist/models/Order.js +1 -0
  136. package/dist/models/Order.js.map +1 -1
  137. package/dist/models/Organization.d.ts +30 -23
  138. package/dist/models/Organization.d.ts.map +1 -1
  139. package/dist/models/Organization.js +113 -61
  140. package/dist/models/Organization.js.map +1 -1
  141. package/dist/models/PasswordToken.d.ts +5 -1
  142. package/dist/models/PasswordToken.d.ts.map +1 -1
  143. package/dist/models/PasswordToken.js +18 -17
  144. package/dist/models/PasswordToken.js.map +1 -1
  145. package/dist/models/Payment.d.ts +35 -3
  146. package/dist/models/Payment.d.ts.map +1 -1
  147. package/dist/models/Payment.js +66 -3
  148. package/dist/models/Payment.js.map +1 -1
  149. package/dist/models/Registration.d.ts +1 -0
  150. package/dist/models/Registration.d.ts.map +1 -1
  151. package/dist/models/Registration.js +4 -3
  152. package/dist/models/Registration.js.map +1 -1
  153. package/dist/models/RegistrationInvitation.d.ts +14 -0
  154. package/dist/models/RegistrationInvitation.d.ts.map +1 -0
  155. package/dist/models/RegistrationInvitation.js +45 -0
  156. package/dist/models/RegistrationInvitation.js.map +1 -0
  157. package/dist/models/STCredit.d.ts +4 -0
  158. package/dist/models/STCredit.d.ts.map +1 -1
  159. package/dist/models/STCredit.js +28 -0
  160. package/dist/models/STCredit.js.map +1 -1
  161. package/dist/models/STInvoice.d.ts +7 -1
  162. package/dist/models/STInvoice.d.ts.map +1 -1
  163. package/dist/models/STInvoice.js +9 -0
  164. package/dist/models/STInvoice.js.map +1 -1
  165. package/dist/models/STPackage.d.ts +4 -0
  166. package/dist/models/STPackage.d.ts.map +1 -1
  167. package/dist/models/STPackage.js +12 -1
  168. package/dist/models/STPackage.js.map +1 -1
  169. package/dist/models/UsedRegisterCode.d.ts +9 -0
  170. package/dist/models/UsedRegisterCode.d.ts.map +1 -1
  171. package/dist/models/UsedRegisterCode.js +31 -0
  172. package/dist/models/UsedRegisterCode.js.map +1 -1
  173. package/dist/models/User.d.ts +1 -1
  174. package/dist/models/User.d.ts.map +1 -1
  175. package/dist/models/User.js +1 -1
  176. package/dist/models/User.js.map +1 -1
  177. package/dist/models/_relations.js +25 -0
  178. package/dist/models/_relations.js.map +1 -1
  179. package/dist/models/addresses/City.d.ts +4 -4
  180. package/dist/models/addresses/City.d.ts.map +1 -1
  181. package/dist/models/addresses/City.js +6 -6
  182. package/dist/models/addresses/City.js.map +1 -1
  183. package/dist/models/addresses/PostalCode.d.ts +2 -2
  184. package/dist/models/addresses/PostalCode.d.ts.map +1 -1
  185. package/dist/models/addresses/PostalCode.js +4 -3
  186. package/dist/models/addresses/PostalCode.js.map +1 -1
  187. package/dist/models/addresses/Street.d.ts +3 -3
  188. package/dist/models/addresses/Street.d.ts.map +1 -1
  189. package/dist/models/addresses/Street.js +4 -4
  190. package/dist/models/addresses/Street.js.map +1 -1
  191. package/dist/models/index.d.ts +2 -0
  192. package/dist/models/index.d.ts.map +1 -1
  193. package/dist/models/index.js +2 -0
  194. package/dist/models/index.js.map +1 -1
  195. package/dist/models/v1GroupMigrationData.d.ts +22 -0
  196. package/dist/models/v1GroupMigrationData.d.ts.map +1 -0
  197. package/dist/models/v1GroupMigrationData.js +48 -0
  198. package/dist/models/v1GroupMigrationData.js.map +1 -0
  199. package/package.json +41 -13
  200. package/src/factories/GroupFactory.ts +4 -6
  201. package/src/factories/OrganizationFactory.ts +12 -4
  202. package/src/factories/RegistrationInvitationFactory.ts +24 -0
  203. package/src/factories/STPackageFactory.ts +2 -2
  204. package/src/factories/UserFactory.ts +4 -5
  205. package/src/factories/index.ts +1 -0
  206. package/src/helpers/EmailBuilder.ts +19 -28
  207. package/src/helpers/Handlebars.ts +10 -1
  208. package/src/helpers/InvoiceCounter.test.ts +220 -0
  209. package/src/helpers/InvoiceCounter.ts +162 -0
  210. package/src/index.ts +0 -1
  211. package/src/migrations/1605262045-import-postcodes.ts +62 -25
  212. package/src/migrations/1605262046-import-postcodes-nl.ts +58 -17
  213. package/src/migrations/1719567881-organization-periodId.sql +2 -0
  214. package/src/migrations/1719567882-groups-periodId.sql +2 -0
  215. package/src/migrations/1720080975-convert-charset.ts +34 -0
  216. package/src/migrations/1720080976-convert-charset-leads.ts +16 -13
  217. package/src/migrations/1721400546-users-memberId.sql +2 -0
  218. package/src/migrations/1722269236-group-waitinglist-id.sql +2 -1
  219. package/src/migrations/1722525785-balance-item-paying-organization-id.sql +2 -0
  220. package/src/migrations/1722525787-depending-balance-item.sql +2 -0
  221. package/src/migrations/1722963554-registration-group-price-and-options.sql +1 -1
  222. package/src/migrations/1723652797-payments-paying-organization-id-fk.sql +2 -0
  223. package/src/migrations/1733317908-added-missing-organization-fk-on-registrations.sql +2 -0
  224. package/src/migrations/1733317910-paying-organization-id-fk.sql +2 -0
  225. package/src/migrations/1733504881-negative-invoice-id.sql +6 -0
  226. package/src/migrations/1733994455-balance-item-status-open.ts +30 -0
  227. package/src/migrations/1763216320-bigint-balance-item-payments.sql +2 -0
  228. package/src/migrations/1763216320-bigint-balance-items.sql +5 -0
  229. package/src/migrations/1763216320-bigint-orders.sql +2 -0
  230. package/src/migrations/1763216320-bigint-payments.sql +2 -0
  231. package/src/migrations/1763216332-bigint-balance-item-price-total.sql +2 -0
  232. package/src/migrations/1769087808-corrected-invoice-user-agent.sql +2 -0
  233. package/src/migrations/1769087809-payments-invoice-id.sql +2 -0
  234. package/src/migrations/1772033555-balance-item-package-id.sql +2 -0
  235. package/src/migrations/1776873089-create-registration-invitations-table.sql +13 -0
  236. package/src/migrations/1778657958-payments-create-mandate.sql +2 -0
  237. package/src/migrations/1778657959-payments-mandate-id.sql +2 -0
  238. package/src/migrations/1778796615-payments-reversing-payment-id.sql +5 -0
  239. package/src/migrations/1778950642-price-invoiced.sql +2 -0
  240. package/src/migrations/1779443446-transfer-fees.sql +3 -0
  241. package/src/migrations/1779709174-used-register-code-balance-item-id.sql +5 -0
  242. package/src/migrations/1779968328-payments-admin-user-id.sql +5 -0
  243. package/src/migrations/1779970611-payments-refunded-amount.sql +2 -0
  244. package/src/migrations/1779972640-balance-items-failed-at.sql +2 -0
  245. package/src/migrations/1780328285-document-template-locked.sql +2 -0
  246. package/src/migrations/1780328286-document-locked.sql +2 -0
  247. package/src/migrations/1780412083-documents-set-locked.ts +18 -0
  248. package/src/migrations/1780928401-v1-groups-migration-data.ts +50 -0
  249. package/src/models/BalanceItem.ts +98 -43
  250. package/src/models/CachedBalance.test.ts +46 -46
  251. package/src/models/CachedBalance.ts +7 -7
  252. package/src/models/Document.ts +34 -13
  253. package/src/models/DocumentTemplate.ts +56 -17
  254. package/src/models/Email.test.ts +3 -3
  255. package/src/models/Email.ts +28 -49
  256. package/src/models/EmailVerificationCode.ts +8 -22
  257. package/src/models/Event.ts +6 -4
  258. package/src/models/EventNotification.ts +6 -6
  259. package/src/models/Group.ts +24 -3
  260. package/src/models/Invoice.ts +10 -9
  261. package/src/models/MemberPlatformMembership.test.ts +70 -0
  262. package/src/models/MemberPlatformMembership.ts +16 -12
  263. package/src/models/MollieToken.ts +42 -102
  264. package/src/models/Order.ts +14 -26
  265. package/src/models/Organization.ts +143 -86
  266. package/src/models/PasswordToken.ts +21 -21
  267. package/src/models/Payment.ts +61 -4
  268. package/src/models/Registration.ts +6 -5
  269. package/src/models/RegistrationInvitation.ts +40 -0
  270. package/src/models/STCredit.ts +32 -0
  271. package/src/models/STInvoice.ts +11 -5
  272. package/src/models/STPackage.ts +19 -14
  273. package/src/models/UsedRegisterCode.ts +34 -0
  274. package/src/models/User.ts +6 -7
  275. package/src/models/_relations.ts +29 -0
  276. package/src/models/addresses/City.ts +8 -6
  277. package/src/models/addresses/PostalCode.test.ts +1 -0
  278. package/src/models/addresses/PostalCode.ts +5 -3
  279. package/src/models/addresses/Street.ts +6 -4
  280. package/src/models/index.ts +3 -0
  281. package/src/models/v1GroupMigrationData.ts +43 -0
  282. package/dist/helpers/MemberMerger.d.ts +0 -14
  283. package/dist/helpers/MemberMerger.d.ts.map +0 -1
  284. package/dist/helpers/MemberMerger.js +0 -364
  285. package/dist/helpers/MemberMerger.js.map +0 -1
  286. package/dist/migrations/1720080975-convert-charset.sql +0 -85
  287. package/dist/migrations/1723202126-member-number-index.sql +0 -2
  288. package/dist/models/OneTimeToken.d.ts +0 -38
  289. package/dist/models/OneTimeToken.d.ts.map +0 -1
  290. package/dist/models/OneTimeToken.js +0 -125
  291. package/dist/models/OneTimeToken.js.map +0 -1
  292. package/src/helpers/MemberMerger.test.ts +0 -782
  293. package/src/helpers/MemberMerger.ts +0 -577
  294. package/src/migrations/1720080975-convert-charset.sql +0 -85
  295. package/src/migrations/1723202126-member-number-index.sql +0 -2
  296. package/src/models/OneTimeToken.ts +0 -133
package/package.json CHANGED
@@ -1,37 +1,65 @@
1
1
  {
2
2
  "name": "@stamhoofd/models",
3
- "version": "2.120.6",
3
+ "version": "2.122.0",
4
4
  "type": "module",
5
- "types": "./dist/index.d.ts",
6
5
  "main": "./dist/index.js",
7
- "license": "UNLICENCED",
6
+ "types": "./dist/index.d.ts",
8
7
  "sideEffects": [
9
8
  "./src/models/_relations.ts",
10
9
  "./dist/models/_relations.js"
11
10
  ],
11
+ "exports": {
12
+ ".": "./dist/index.js",
13
+ "./*.ts": "./dist/*.js",
14
+ "./*.js": "./dist/*.js"
15
+ },
16
+ "imports": {
17
+ "#*": "./src/*.ts",
18
+ "#*.ts": "./src/*.ts"
19
+ },
12
20
  "files": [
13
21
  "dist",
14
22
  "src"
15
23
  ],
16
24
  "scripts": {
17
25
  "build": "tsc --build tsconfig.build.json && mkdir -p ./dist/migrations && rsync --delete --exclude='*.ts' --exclude='*.js' --exclude='*.map' -r --checksum ./src/migrations/ ./dist/migrations/ && rsync -r --checksum ./src/assets/ ./dist/assets/",
26
+ "lint": "eslint",
18
27
  "test": "vitest",
19
- "lint": "eslint"
20
- },
21
- "peerDependencies": {
22
- "@simonbackx/simple-database": "1.36.12",
23
- "@stamhoofd/email": "*",
24
- "@stamhoofd/queues": "*"
28
+ "test:coverage": "vitest --coverage"
25
29
  },
26
30
  "dependencies": {
27
- "@aws-sdk/client-s3": "^3.839.0",
28
- "base-x": "^3.0.11"
31
+ "@aws-sdk/client-s3": "3.1068.0",
32
+ "@aws-sdk/client-sesv2": "3.1068.0",
33
+ "@bwip-js/node": "4.11.1",
34
+ "@mollie/api-client": "4.5.0",
35
+ "@simonbackx/simple-encoding": "2.26.10",
36
+ "@simonbackx/simple-endpoints": "1.21.1",
37
+ "@simonbackx/simple-errors": "1.5.0",
38
+ "argon2": "0.44.0",
39
+ "base-x": "3.0.11",
40
+ "handlebars": "4.7.9",
41
+ "sharp": "0.35.1"
29
42
  },
30
43
  "devDependencies": {
31
- "@simonbackx/simple-database": "1.36.12"
44
+ "@simonbackx/simple-database": "1.37.1",
45
+ "@stamhoofd/test-utils": "2.122.0",
46
+ "@types/luxon": "3.7.1",
47
+ "@types/uuid": "8.3.4",
48
+ "vitest": "4.1.8"
49
+ },
50
+ "peerDependencies": {
51
+ "@simonbackx/simple-database": "1.37.1",
52
+ "@stamhoofd/backend-i18n": "*",
53
+ "@stamhoofd/email": "*",
54
+ "@stamhoofd/queues": "*",
55
+ "@stamhoofd/sql": "*",
56
+ "@stamhoofd/structures": "*",
57
+ "@stamhoofd/types": "*",
58
+ "@stamhoofd/utility": "*"
32
59
  },
60
+ "license": "UNLICENCED",
33
61
  "publishConfig": {
34
62
  "access": "public"
35
63
  },
36
- "gitHead": "0f41cc566739b9793910b67286a66be5745a163e"
64
+ "gitHead": "3c022b7d7801fb105acfaa401432a8d029abda61"
37
65
  }
@@ -1,5 +1,5 @@
1
1
  import { Factory } from '@simonbackx/simple-database';
2
- import type { BundleDiscount, GroupPriceDiscount, GroupType} from '@stamhoofd/structures';
2
+ import type { BundleDiscount, GroupPriceDiscount, GroupType } from '@stamhoofd/structures';
3
3
  import { BundleDiscountGroupPriceSettings, GroupPrice, GroupSettings, ReduceablePrice, TranslatedString } from '@stamhoofd/structures';
4
4
 
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
@@ -42,15 +42,14 @@ export class GroupFactory extends Factory<Options, Group> {
42
42
  });
43
43
  }
44
44
  group.periodId = this.options.period.id;
45
- }
46
- else {
45
+ } else {
47
46
  group.periodId = organization.periodId;
48
47
  }
49
48
 
50
49
  group.waitingListId = this.options.waitingListId ?? null;
51
50
 
52
51
  group.settings = GroupSettings.create({
53
- name: this.options.name ?? new TranslatedString('Group name'),
52
+ name: this.options.name ?? new TranslatedString('Group ' + this.randomString(5)),
54
53
  startDate: new Date(new Date().getTime() - 10 * 1000),
55
54
  endDate: new Date(new Date().getTime() + 10 * 1000),
56
55
  registrationStartDate: new Date(new Date().getTime() - 10 * 1000),
@@ -80,8 +79,7 @@ export class GroupFactory extends Factory<Options, Group> {
80
79
  for (const discount of this.options.bundleDiscounts) {
81
80
  map.set(discount, null);
82
81
  }
83
- }
84
- else {
82
+ } else {
85
83
  map = this.options.bundleDiscounts;
86
84
  }
87
85
 
@@ -1,5 +1,5 @@
1
1
  import { Factory } from '@simonbackx/simple-database';
2
- import type { PermissionRoleDetailed } from '@stamhoofd/structures';
2
+ import type { PermissionRoleDetailed, STPackageBundle, STPackageType } from '@stamhoofd/structures';
3
3
  import { Address, OrganizationMetaData, OrganizationType } from '@stamhoofd/structures';
4
4
  import { Formatter } from '@stamhoofd/utility';
5
5
 
@@ -7,6 +7,7 @@ import { Organization } from '../models/Organization.js';
7
7
  import type { RegistrationPeriod } from '../models/RegistrationPeriod.js';
8
8
  import { RegistrationPeriodFactory } from './RegistrationPeriodFactory.js';
9
9
  import { Country } from '@stamhoofd/types/Country';
10
+ import { STPackageFactory } from './STPackageFactory.js';
10
11
 
11
12
  class Options {
12
13
  uri?: string;
@@ -17,12 +18,13 @@ class Options {
17
18
  roles?: PermissionRoleDetailed[];
18
19
  period?: RegistrationPeriod;
19
20
  tags?: string[];
21
+ packages?: STPackageBundle[];
20
22
  }
21
23
 
22
24
  export class OrganizationFactory extends Factory<Options, Organization> {
23
25
  async create(): Promise<Organization> {
24
26
  const organization = new Organization();
25
- organization.name = this.options.name ?? 'Organization ' + (new Date().getTime() + Math.floor(Math.random() * 999999));
27
+ organization.name = this.options.name ?? 'Organization ' + (new Date().getTime() + '-' + Math.floor(Math.random() * 999999) + '-' + Math.floor(Math.random() * 999999));
26
28
  organization.website = 'https://domain.com';
27
29
  organization.registerDomain = this.options.domain ?? null;
28
30
  organization.uri = this.options.uri ?? Formatter.slug(organization.name);
@@ -45,8 +47,7 @@ export class OrganizationFactory extends Factory<Options, Organization> {
45
47
 
46
48
  if (this.options.period) {
47
49
  period = this.options.period;
48
- }
49
- else {
50
+ } else {
50
51
  period = await new RegistrationPeriodFactory({}).create();
51
52
  }
52
53
 
@@ -68,6 +69,13 @@ export class OrganizationFactory extends Factory<Options, Organization> {
68
69
  await period.save();
69
70
  }
70
71
 
72
+ for (const bundle of this.options.packages ?? []) {
73
+ await new STPackageFactory({
74
+ organization,
75
+ bundle,
76
+ }).create();
77
+ }
78
+
71
79
  return organization;
72
80
  }
73
81
  }
@@ -0,0 +1,24 @@
1
+ import { Factory } from '@simonbackx/simple-database';
2
+
3
+ import type { Group } from '../models/Group.js';
4
+ import type { Member } from '../models/Member.js';
5
+ import type { Organization } from '../models/Organization.js';
6
+ import { RegistrationInvitation } from '../models/RegistrationInvitation.js';
7
+
8
+ class Options {
9
+ group: Group;
10
+ member: Member;
11
+ organization: Organization
12
+ }
13
+
14
+ export class RegistrationInvitationFactory extends Factory<Options, RegistrationInvitation> {
15
+ async create(): Promise<RegistrationInvitation> {
16
+ const invitation = new RegistrationInvitation();
17
+ invitation.groupId = this.options.group.id;
18
+ invitation.memberId = this.options.member.id;
19
+ invitation.organizationId = this.options.organization.id;
20
+ await invitation.save();
21
+
22
+ return invitation;
23
+ }
24
+ }
@@ -24,8 +24,7 @@ export class STPackageFactory extends Factory<Options, STPackage> {
24
24
 
25
25
  if (this.options.removeAt) {
26
26
  pack.validAt = new Date(this.options.removeAt.getTime() - 365 * 1000 * 60 * 60 * 24);
27
- }
28
- else {
27
+ } else {
29
28
  pack.validAt = this.options.validAt !== undefined ? this.options.validAt : new Date();
30
29
  }
31
30
 
@@ -38,6 +37,7 @@ export class STPackageFactory extends Factory<Options, STPackage> {
38
37
  }
39
38
 
40
39
  await pack.save();
40
+
41
41
  return pack;
42
42
  }
43
43
  }
@@ -1,5 +1,5 @@
1
1
  import { Factory } from '@simonbackx/simple-database';
2
- import type { Permissions} from '@stamhoofd/structures';
2
+ import type { Permissions } from '@stamhoofd/structures';
3
3
  import { NewUser, UserPermissions } from '@stamhoofd/structures';
4
4
 
5
5
  import type { Organization } from '../models/Organization.js';
@@ -22,13 +22,12 @@ class Options {
22
22
 
23
23
  export class UserFactory extends Factory<Options, User> {
24
24
  async create(): Promise<User> {
25
- let organization: Organization | null = null;
25
+ let organization: Organization | null;
26
26
 
27
27
  if (!this.options.organization && STAMHOOFD.userMode !== 'platform' && !this.options.globalPermissions) {
28
28
  const organizationFactory = new OrganizationFactory({});
29
29
  organization = await organizationFactory.create();
30
- }
31
- else {
30
+ } else {
32
31
  organization = this.options.organization ?? null;
33
32
  }
34
33
 
@@ -40,7 +39,7 @@ export class UserFactory extends Factory<Options, User> {
40
39
  organizationId: organization?.id ?? null,
41
40
  password,
42
41
  }), {
43
- allowPlatform: !!this.options.globalPermissions
42
+ allowPlatform: !!this.options.globalPermissions,
44
43
  });
45
44
  if (!user) {
46
45
  throw new Error('Unexpected failure when creating user in factory');
@@ -24,3 +24,4 @@ export * from './RecordCategoryFactory.js';
24
24
  export * from './RecordAnswerFactory.js';
25
25
  export * from './DocumentTemplateFactory.js';
26
26
  export * from './STPackageFactory.js';
27
+ export * from './RegistrationInvitationFactory.js';
@@ -1,7 +1,7 @@
1
1
  import type { EmailBuilder, EmailInterfaceRecipient } from '@stamhoofd/email';
2
2
  import { Email, EmailAddress } from '@stamhoofd/email';
3
- import type { EmailRecipient as EmailRecipientStruct, EmailTemplateType, OrganizationEmail, Platform as PlatformStruct, Recipient} from '@stamhoofd/structures';
4
- import { BalanceItem as BalanceItemStruct, ReceivableBalanceType, replaceEmailHtml, replaceEmailText, Replacement } from '@stamhoofd/structures';
3
+ import type { EmailRecipient as EmailRecipientStruct, EmailTemplateType, OrganizationEmail, Platform as PlatformStruct, Recipient } from '@stamhoofd/structures';
4
+ import { BalanceItem as BalanceItemStruct, getAppHost, ReceivableBalanceType, replaceEmailHtml, replaceEmailText, Replacement } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
 
7
7
  import { SimpleError } from '@simonbackx/simple-errors';
@@ -13,7 +13,7 @@ import type { Group } from '../models/Group.js';
13
13
  import type { Organization } from '../models/Organization.js';
14
14
  import { Platform } from '../models/Platform.js';
15
15
  import { User } from '../models/User.js';
16
- import type {Webshop} from '../models/Webshop.js';
16
+ import type { Webshop } from '../models/Webshop.js';
17
17
 
18
18
  export type EmailTemplateOptions = {
19
19
  type: EmailTemplateType;
@@ -314,8 +314,7 @@ export async function getEmailBuilder(organization: Organization | null, email:
314
314
  'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
315
315
  };
316
316
  cleaned.push(recipient);
317
- }
318
- catch (e) {
317
+ } catch (e) {
319
318
  console.error(e);
320
319
  }
321
320
  }
@@ -499,14 +498,14 @@ export function stripSensitiveRecipientReplacements(recipient: Recipient | Email
499
498
  }
500
499
 
501
500
  // Add dummy unsubscribeUrl
502
- const dummyUnsubscribeUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? organization.getHost() : STAMHOOFD.domains.dashboard) + '/unsubscribe?token=example';
501
+ const dummyUnsubscribeUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? getAppHost('registration', organization, false) : STAMHOOFD.domains.dashboard) + '/unsubscribe?token=example';
503
502
  recipient.replacements.push(Replacement.create({
504
503
  token: 'unsubscribeUrl',
505
504
  value: dummyUnsubscribeUrl,
506
505
  }));
507
506
 
508
507
  // dummy signInUrl
509
- const dummySignInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? organization.getHost() : STAMHOOFD.domains.dashboard) + '/login';
508
+ const dummySignInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? getAppHost('registration', organization, false) : STAMHOOFD.domains.dashboard) + '/login';
510
509
  recipient.replacements.push(Replacement.create({
511
510
  token: 'signInUrl',
512
511
  value: dummySignInUrl,
@@ -536,7 +535,7 @@ export function stripRecipientReplacementsForWebDisplay(recipient: Recipient | E
536
535
  recipient.replacements = recipient.replacements.filter(r => r.token !== 'unsubscribeUrl' && r.token !== 'loginDetails' && r.token !== 'greeting');
537
536
 
538
537
  // Add dummy unsubscribeUrl
539
- const dummyUnsubscribeUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? organization.getHost() : STAMHOOFD.domains.dashboard);
538
+ const dummyUnsubscribeUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? getAppHost('registration', organization, false) : STAMHOOFD.domains.dashboard);
540
539
  recipient.replacements.push(Replacement.create({
541
540
  token: 'unsubscribeUrl',
542
541
  value: dummyUnsubscribeUrl,
@@ -575,7 +574,7 @@ export async function fillRecipientReplacements(recipient: Recipient | EmailReci
575
574
  }
576
575
 
577
576
  if (!recipient.email && !recipient.userId) {
578
- const signInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? organization.getHost() : STAMHOOFD.domains.dashboard) + '/login';
577
+ const signInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? getAppHost('registration', organization, false) : STAMHOOFD.domains.dashboard) + '/login';
579
578
  recipient.replacements.push(Replacement.create({
580
579
  token: 'signInUrl',
581
580
  value: signInUrl,
@@ -587,8 +586,7 @@ export async function fillRecipientReplacements(recipient: Recipient | EmailReci
587
586
  value: '',
588
587
  }));
589
588
  }
590
- }
591
- else {
589
+ } else {
592
590
  // Default signInUrl
593
591
  recipientUser = recipient.userId ? await User.select().where('id', recipient.userId).first(false) : await User.getForAuthentication(organization?.id ?? null, recipient.email!, { allowWithoutAccount: true });
594
592
  if (STAMHOOFD.userMode !== 'platform' && recipientUser && recipientUser.organizationId && recipientUser.organizationId !== (organization?.id ?? null)) {
@@ -600,14 +598,12 @@ export async function fillRecipientReplacements(recipient: Recipient | EmailReci
600
598
  if (!recipientUser || !recipientUser.hasAccount()) {
601
599
  // We can create a special token
602
600
  if (recipientUser) {
603
- signInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? organization.getHost() : STAMHOOFD.domains.dashboard) + '/account-aanmaken?email=' + encodeURIComponent(recipientUser?.email);
604
- }
605
- else {
606
- signInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? organization.getHost() : STAMHOOFD.domains.dashboard) + '/account-aanmaken';
601
+ signInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? getAppHost('registration', organization, false) : STAMHOOFD.domains.dashboard) + '/account-aanmaken?email=' + encodeURIComponent(recipientUser?.email);
602
+ } else {
603
+ signInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? getAppHost('registration', organization, false) : STAMHOOFD.domains.dashboard) + '/account-aanmaken';
607
604
  }
608
- }
609
- else {
610
- signInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? organization.getHost() : STAMHOOFD.domains.dashboard) + '/login?email=' + encodeURIComponent(recipientUser.email);
605
+ } else {
606
+ signInUrl = 'https://' + (organization && STAMHOOFD.userMode === 'organization' ? getAppHost('registration', organization, false) : STAMHOOFD.domains.dashboard) + '/login?email=' + encodeURIComponent(recipientUser.email);
611
607
  }
612
608
 
613
609
  recipient.replacements.push(Replacement.create({
@@ -638,8 +634,7 @@ export async function fillRecipientReplacements(recipient: Recipient | EmailReci
638
634
  }),
639
635
  );
640
636
  }
641
- }
642
- else {
637
+ } else {
643
638
  console.log('No member found for user', recipientUser.id);
644
639
  }
645
640
  }
@@ -653,8 +648,7 @@ export async function fillRecipientReplacements(recipient: Recipient | EmailReci
653
648
  : `<p class="description"><em>${$t('%1EB', { email: emailEscaped })}${suffix}</em></p>`,
654
649
  }),
655
650
  );
656
- }
657
- else {
651
+ } else {
658
652
  if (recipient.email) {
659
653
  const emailEscaped = `<strong>${Formatter.escapeHtml(recipient.email)}</strong>`;
660
654
  console.log('No user found for email', recipient.email);
@@ -665,8 +659,7 @@ export async function fillRecipientReplacements(recipient: Recipient | EmailReci
665
659
  html: `<p class="description"><em>${$t('%1EB', { email: emailEscaped })}</em></p>`,
666
660
  }),
667
661
  );
668
- }
669
- else {
662
+ } else {
670
663
  recipient.replacements.push(
671
664
  Replacement.create({
672
665
  token: 'loginDetails',
@@ -702,8 +695,7 @@ export async function fillRecipientReplacements(recipient: Recipient | EmailReci
702
695
  html: BalanceItemStruct.getDetailsHTMLTable(balanceItems),
703
696
  }),
704
697
  );
705
- }
706
- else {
698
+ } else {
707
699
  recipient.replacements.push(
708
700
  Replacement.create({
709
701
  token: 'outstandingBalance',
@@ -739,8 +731,7 @@ export async function fillRecipientReplacements(recipient: Recipient | EmailReci
739
731
 
740
732
  if (recipient instanceof EmailRecipient) {
741
733
  recipient.replacements.push(...recipient.getRecipient().getDefaultReplacements());
742
- }
743
- else {
734
+ } else {
744
735
  recipient.replacements.push(...recipient.getDefaultReplacements());
745
736
  }
746
737
 
@@ -50,7 +50,12 @@ class AsyncResolver {
50
50
 
51
51
  Handlebars.registerHelper('eq', (a, b) => a == b);
52
52
  Handlebars.registerHelper('neq', (a, b) => a !== b);
53
- Handlebars.registerHelper('formatPrice', a => typeof a === 'number' ? Formatter.price(a) : a);
53
+ Handlebars.registerHelper('formatPrice', (a, options) => {
54
+ if (typeof a !== 'number') return a;
55
+ // round=true rounds to nearest cent (100 units = 1 cent, where 10000 units = €1)
56
+ const value = options?.hash?.round ? Math.round(a / 100) * 100 : a;
57
+ return Formatter.price(value);
58
+ });
54
59
  Handlebars.registerHelper('formatDate', (a, options) => {
55
60
  if (!(a instanceof Date)) {
56
61
  return '';
@@ -103,6 +108,10 @@ Handlebars.registerHelper('coalesce', (...args) => {
103
108
  return args.find(a => a !== null && a !== undefined) ?? null;
104
109
  });
105
110
 
111
+ Handlebars.registerHelper('$t', (t: string) => {
112
+ return $t(t);
113
+ });
114
+
106
115
  Handlebars.registerHelper('days', (a, b) => {
107
116
  if (!(a instanceof Date) || !(b instanceof Date)) {
108
117
  return 0;
@@ -0,0 +1,220 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { InvoiceCounter } from './InvoiceCounter.js';
3
+ import { OrganizationInvoiceSettings } from '@stamhoofd/structures/OrganizationInvoiceSettings.js';
4
+ import { Invoice } from '../models/Invoice.js';
5
+ import { OrganizationFactory } from '../factories/OrganizationFactory.js';
6
+
7
+ // Minimal settings factory
8
+ function makeSettings(overrides: Partial<OrganizationInvoiceSettings> = {}): OrganizationInvoiceSettings {
9
+ return OrganizationInvoiceSettings.create({
10
+ resetMonth: null,
11
+ prefixYear: false,
12
+ ...overrides,
13
+ })
14
+ }
15
+
16
+ // ─────────────────────────────────────────────
17
+ // parseNumber
18
+ // ─────────────────────────────────────────────
19
+ describe('InvoiceCounter.parseNumber', () => {
20
+ describe('without year prefix', () => {
21
+ const settings = makeSettings({ prefixYear: false });
22
+
23
+ it('parses ABC-123 → 123', () => {
24
+ expect(InvoiceCounter.parseNumber(settings, 'ABC-123')).toBe(123);
25
+ });
26
+
27
+ it('parses XXX0001 → 1', () => {
28
+ expect(InvoiceCounter.parseNumber(settings, 'XXX0001')).toBe(1);
29
+ });
30
+
31
+ it('parses 05STA0001 → 1', () => {
32
+ expect(InvoiceCounter.parseNumber(settings, '05STA0001')).toBe(1);
33
+ });
34
+
35
+ it('parses 1234-0011 → 11', () => {
36
+ expect(InvoiceCounter.parseNumber(settings, '1234-0011')).toBe(11);
37
+ });
38
+ });
39
+
40
+ describe('with year prefix (prefixYear: true)', () => {
41
+ const settings = makeSettings({ prefixYear: true });
42
+
43
+ it('parses 2012001584 → 1584', () => {
44
+ expect(InvoiceCounter.parseNumber(settings, '2012001584')).toBe(1584);
45
+ });
46
+
47
+ it('parses XXX-2012001584 → 1584', () => {
48
+ expect(InvoiceCounter.parseNumber(settings, 'XXX-2012001584')).toBe(1584);
49
+ });
50
+ });
51
+ });
52
+
53
+ describe('InvoiceCounter.formatNumber', () => {
54
+ const date2025 = new Date('2025-06-15T12:00:00Z');
55
+
56
+ it('formats 123 with fixed prefix ABC → "ABC000123"', () => {
57
+ const settings = makeSettings({ fixedPrefix: 'ABC' });
58
+ expect(InvoiceCounter.formatNumber(settings, 123, date2025)).toBe('ABC000123');
59
+ });
60
+
61
+ it('formats 123 with fixed prefix ABC1 → "ABC1-000123"', () => {
62
+ const settings = makeSettings({ fixedPrefix: 'ABC1' });
63
+ expect(InvoiceCounter.formatNumber(settings, 123, date2025)).toBe('ABC1-000123');
64
+ });
65
+
66
+ it('formats 1 with year prefix → "2025000001"', () => {
67
+ const settings = makeSettings({ prefixYear: true });
68
+ expect(InvoiceCounter.formatNumber(settings, 1, date2025)).toBe('2025000001');
69
+ });
70
+
71
+ it('formats 11 with fixed prefix "111-" and no duplicate dash → "111-000011"', () => {
72
+ const settings = makeSettings({ fixedPrefix: '111-' });
73
+ expect(InvoiceCounter.formatNumber(settings, 11, date2025)).toBe('111-000011');
74
+ });
75
+
76
+ it('formats 11 with fixed prefix "test-" and no duplicate dash → "test-000011"', () => {
77
+ const settings = makeSettings({ fixedPrefix: 'test-' });
78
+ expect(InvoiceCounter.formatNumber(settings, 11, date2025)).toBe('test-000011');
79
+ });
80
+
81
+ it('formats 54 with year prefix + fixed prefix → "ABC202500054"', () => {
82
+ // year is prepended first, then fixedPrefix wraps around
83
+ const settings = makeSettings({ prefixYear: true, fixedPrefix: 'ABC' });
84
+ expect(InvoiceCounter.formatNumber(settings, 54, date2025)).toBe('ABC2025000054');
85
+ });
86
+ });
87
+
88
+ describe('InvoiceCounter.shouldStartNewSeries', () => {
89
+ it('returns false when resetMonth is null', () => {
90
+ const settings = makeSettings({ resetMonth: null });
91
+ const last = new Date('2024-12-01');
92
+ const now = new Date('2025-06-01');
93
+ expect(InvoiceCounter.shouldStartNewSeries(settings, last, now)).toBe(false);
94
+ });
95
+
96
+ it('returns true when crossing the reset month boundary', () => {
97
+ // reset on January (month 1)
98
+ const settings = makeSettings({ resetMonth: 1 });
99
+ const last = new Date('2024-06-15');
100
+ const now = new Date('2025-01-02');
101
+ expect(InvoiceCounter.shouldStartNewSeries(settings, last, now)).toBe(true);
102
+ });
103
+
104
+ it('returns false when still within the same series period', () => {
105
+ const settings = makeSettings({ resetMonth: 1 });
106
+ const last = new Date('2025-01-05');
107
+ const now = new Date('2025-06-01');
108
+ expect(InvoiceCounter.shouldStartNewSeries(settings, last, now)).toBe(false);
109
+ });
110
+ });
111
+
112
+ describe('InvoiceCounter.assignNextNumber', () => {
113
+ beforeEach(() => {
114
+ InvoiceCounter.clearAll();
115
+ vitest.useFakeTimers({ toFake: ['Date'] });
116
+ });
117
+
118
+ afterEach(() => {
119
+ vitest.useRealTimers();
120
+ });
121
+
122
+ it('assigns 000001 to the first invoice for an org (no cache, no DB)', async () => {
123
+ const org = await new OrganizationFactory({}).create();
124
+ const settings = makeSettings({});
125
+
126
+ const invoice = new Invoice();
127
+ invoice.organizationId = org.id;
128
+ await InvoiceCounter.assignNextNumber(invoice, settings);
129
+
130
+ expect(invoice.number).toBe('000001');
131
+ expect(invoice.invoicedAt).not.toBeNull();
132
+ });
133
+
134
+ it('increments from cache when called twice without a reset boundary', async () => {
135
+ const org = await new OrganizationFactory({}).create();
136
+ const settings = makeSettings({});
137
+
138
+ const invoice1 = new Invoice();
139
+ invoice1.organizationId = org.id;
140
+ await InvoiceCounter.assignNextNumber(invoice1, settings);
141
+ expect(invoice1.number).toBe('000001');
142
+
143
+ const invoice2 = new Invoice();
144
+ invoice2.organizationId = org.id;
145
+ await InvoiceCounter.assignNextNumber(invoice2, settings);
146
+ expect(invoice2.number).toBe('000002');
147
+ });
148
+
149
+ it('continues from DB when cache is absent but a numbered invoice exists', async () => {
150
+ const org = await new OrganizationFactory({}).create();
151
+ const settings = makeSettings({});
152
+
153
+ const existing = new Invoice();
154
+ existing.organizationId = org.id;
155
+ existing.number = '000042';
156
+ existing.invoicedAt = new Date();
157
+ await existing.save();
158
+
159
+ InvoiceCounter.clearAll();
160
+
161
+ const invoice = new Invoice();
162
+ invoice.organizationId = org.id;
163
+ await InvoiceCounter.assignNextNumber(invoice, settings);
164
+
165
+ expect(invoice.number).toBe('000043');
166
+ });
167
+
168
+ it('starts a new series when the reset boundary is crossed (cache hit)', async () => {
169
+ const org = await new OrganizationFactory({}).create();
170
+ const settings = makeSettings({ resetMonth: 1, prefixYear: true });
171
+
172
+ vitest.setSystemTime(new Date('2024-06-15T12:00:00Z'));
173
+ const invoice1 = new Invoice();
174
+ invoice1.organizationId = org.id;
175
+ await InvoiceCounter.assignNextNumber(invoice1, settings);
176
+ expect(invoice1.number).toBe('2024000001');
177
+
178
+ // Advance past the January 2025 reset boundary
179
+ vitest.setSystemTime(new Date('2025-02-01T12:00:00Z'));
180
+ const invoice2 = new Invoice();
181
+ invoice2.organizationId = org.id;
182
+ await InvoiceCounter.assignNextNumber(invoice2, settings);
183
+
184
+ expect(invoice2.number).toBe('2025000001');
185
+ });
186
+
187
+ it('falls back to 1 when the DB invoice number cannot be parsed', async () => {
188
+ const org = await new OrganizationFactory({}).create();
189
+ const settings = makeSettings({});
190
+
191
+ const existing = new Invoice();
192
+ existing.organizationId = org.id;
193
+ existing.number = 'INVALID';
194
+ existing.invoicedAt = new Date();
195
+ await existing.save();
196
+
197
+ const invoice = new Invoice();
198
+ invoice.organizationId = org.id;
199
+ await InvoiceCounter.assignNextNumber(invoice, settings);
200
+
201
+ expect(invoice.number).toBe('000001');
202
+ });
203
+
204
+ it('reads from DB after resetNumbers clears the cache', async () => {
205
+ const org = await new OrganizationFactory({}).create();
206
+ const settings = makeSettings({});
207
+
208
+ const invoice1 = new Invoice();
209
+ invoice1.organizationId = org.id;
210
+ await InvoiceCounter.assignNextNumber(invoice1, settings);
211
+ expect(invoice1.number).toBe('000001');
212
+
213
+ await InvoiceCounter.resetNumbers(org.id);
214
+
215
+ const invoice2 = new Invoice();
216
+ invoice2.organizationId = org.id;
217
+ await InvoiceCounter.assignNextNumber(invoice2, settings);
218
+ expect(invoice2.number).toBe('000002');
219
+ });
220
+ });