| 1 | type ArgSpec = |
| 2 | | { type |
| 3 | | { |
| 4 | type: 'number |
| 5 | default: number |
| 6 | min: number |
| 7 | max: number |
| 8 | } |
| 9 | | { |
| 10 | type: 'enum |
| 11 | default: string |
| 12 | options: { |
| 13 | } |
| 14 | |
| 15 | export interface InvoiceTemplateVariable |
| 16 | arg: ArgSpec |
| 17 | description: string |
| 18 | id: string |
| 19 | label: string |
| 20 | sample: string |
| 21 | } |
| 22 | |
| 23 | const VARIABLES: InvoiceTemplateVariable |
| 24 | { |
| 25 | id: 'seq', |
| 26 | label: 'Sequence', |
| 27 | description: |
| 28 | 'Zero-padded running counter that increments with each invoice.', |
| 29 | sample: '001', |
| 30 | arg: { type: ' |
| 31 | }, |
| 32 | { |
| 33 | id: 'day', |
| 34 | label: 'Day', |
| 35 | description: 'Day of the month, zero-padded (01–31).', |
| 36 | sample: '07', |
| 37 | arg: { type: ' |
| 38 | }, |
| 39 | { |
| 40 | id: 'month', |
| 41 | label: 'Month', |
| 42 | description: 'Month when the invoice is created.', |
| 43 | sample: '04', |
| 44 | arg: { |
| 45 | type: 'enum', |
| 46 | default: 'num', |
| 47 | options: [ |
| 48 | { value: 'num', |
| 49 | { value: 'short', |
| 50 | { value: 'long', |
| 51 | ], |
| 52 | }, |
| 53 | }, |
| 54 | { |
| 55 | id: 'year', |
| 56 | label: 'Year', |
| 57 | description: 'Year when the invoice is created.', |
| 58 | sample: '2026', |
| 59 | arg: { |
| 60 | type: 'enum', |
| 61 | default: 'full', |
| 62 | options: [ |
| 63 | { value: 'full', |
| 64 | { value: 'short', |
| 65 | ], |
| 66 | }, |
| 67 | }, |
| 68 | { |
| 69 | id: 'hour', |
| 70 | label: 'Hour', |
| 71 | description: 'Hour when the invoice is created.', |
| 72 | sample: '14', |
| 73 | arg: { |
| 74 | type: 'enum', |
| 75 | default: '24', |
| 76 | options: [ |
| 77 | { value: '24', |
| 78 | { value: '12', |
| 79 | ], |
| 80 | }, |
| 81 | }, |
| 82 | { |
| 83 | id: 'min', |
| 84 | label: 'Minute', |
| 85 | description: 'Minute when the invoice is created, zero-padded (00–59).', |
| 86 | sample: '30', |
| 87 | arg: { type: ' |
| 88 | }, |
| 89 | { |
| 90 | id: 'sec', |
| 91 | label: 'Second', |
| 92 | description: 'Second when the invoice is created, zero-padded (00–59).', |
| 93 | sample: '45', |
| 94 | arg: { type: ' |
| 95 | }, |
| 96 | { |
| 97 | id: 'meridiem', |
| 98 | label: 'AM/PM', |
| 99 | description: 'Meridiem indicator. Pair with the 12-hour variant of Hour.', |
| 100 | sample: 'pm', |
| 101 | arg: { type: ' |
| 102 | }, |
| 103 | ] |
| 104 | |
| 105 | const TOKEN_PREFIX = ' |
| 106 | const TOKEN_REGEX = / |
| 107 | |
| 108 | const VARIABLE_MAP = new |
| 109 | |
| 110 | function buildExpression( |
| 111 | variable: InvoiceTemplateVariable |
| 112 | value?: string |
| 113 | ): string { |
| 114 | if (variable.arg |
| 115 | return `${TOKEN_PREFIX}${ |
| 116 | } |
| 117 | const resolved = |
| 118 | return `${TOKEN_PREFIX}${ |
| 119 | } |
| 120 | |
| 121 | function formatPart( |
| 122 | date: Date |
| 123 | options: Intl |
| 124 | partType: Intl |
| 125 | timezone?: string |
| 126 | ): string { |
| 127 | const formatter = |
| 128 | ...options, |
| 129 | ...(timezone ? |
| 130 | }) |
| 131 | return ( |
| 132 | formatter.formatToParts(date |
| 133 | ) |
| 134 | } |
| 135 | |
| 136 | function resolveToken( |
| 137 | id: string, |
| 138 | rawArg: string |
| 139 | date: Date |
| 140 | timezone: string |
| 141 | sequence: number |
| 142 | ): string | null |
| 143 | const variable = |
| 144 | if (!variable) |
| 145 | return null |
| 146 | } |
| 147 | const arg = |
| 148 | rawArg ?? |
| 149 | (variable.arg.type |
| 150 | |
| 151 | switch (id) { |
| 152 | case 'seq': { |
| 153 | const digits = |
| 154 | return String(sequence |
| 155 | } |
| 156 | case 'day': |
| 157 | return formatPart(date |
| 158 | case 'month': { |
| 159 | if (arg === |
| 160 | return formatPart(date |
| 161 | } |
| 162 | return formatPart(date |
| 163 | } |
| 164 | case 'year': { |
| 165 | const full = |
| 166 | return arg === |
| 167 | } |
| 168 | case 'hour': { |
| 169 | if (arg === |
| 170 | const hour = |
| 171 | date, |
| 172 | { hour: '2-digit', |
| 173 | 'hour', |
| 174 | timezone |
| 175 | ) |
| 176 | return hour.padStart |
| 177 | } |
| 178 | return formatPart( |
| 179 | date, |
| 180 | { hour: '2-digit', |
| 181 | 'hour', |
| 182 | timezone |
| 183 | ) |
| 184 | } |
| 185 | case 'min': |
| 186 | return formatPart(date |
| 187 | case 'sec': |
| 188 | return formatPart(date |
| 189 | case 'meridiem': |
| 190 | return formatPart( |
| 191 | date, |
| 192 | { hour: '2-digit', |
| 193 | 'dayPeriod', |
| 194 | timezone |
| 195 | ).toLowerCase() |
| 196 | default: |
| 197 | return null |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | export const InvoiceNumberGeneratorEngine = |
| 202 | getAvailableVariables(): InvoiceTemplateVariable |
| 203 | return VARIABLES |
| 204 | }, |
| 205 | buildExpression, |
| 206 | generateInvoiceNumber( |
| 207 | expression: string, |
| 208 | options: { |
| 209 | ): string { |
| 210 | const date = |
| 211 | const sequence = |
| 212 | return expression.replace |
| 213 | TOKEN_REGEX, |
| 214 | (match, id: |
| 215 | const resolved = |
| 216 | id, |
| 217 | rawArg, |
| 218 | date, |
| 219 | options.timezone, |
| 220 | sequence |
| 221 | ) |
| 222 | return resolved ?? |
| 223 | } |
| 224 | ) |
| 225 | }, |
| 226 | } |