-
Notifications
You must be signed in to change notification settings - Fork 0
OUT-3710: walk DocNumber + atomic claim to fix QBO duplicate-DocNumber collisions #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
b07e6b8
feat(OUT-3710): add formatAssemblyInvoicePrivateNote helper
SandipBajracharya b3da507
feat(OUT-3710): add findInvoicesByDocNumberPrefix to IntuitAPI
SandipBajracharya d213359
feat(OUT-3710): add findNextAvailableDocNumber walker helper
SandipBajracharya 40a5052
feat(OUT-3710): require PrivateNote on QBInvoiceCreatePayloadSchema
SandipBajracharya 24d3ce1
feat(OUT-3710): walk DocNumber + stamp PrivateNote on invoice create
SandipBajracharya 694cb93
feat(OUT-3710): scoped partial unique index on qb_sync_logs
SandipBajracharya 0f0a081
feat(OUT-3710): migration — scoped unique index on qb_sync_logs
SandipBajracharya 5b1296f
feat(OUT-3710): atomic claimWebhookEvent via partial unique index
SandipBajracharya e8584df
test(OUT-3710): atomic claimWebhookEvent + scoped partial-index behavior
SandipBajracharya f1f6eea
chore(OUT-3710): lint/format
SandipBajracharya 078b9e6
fix(OUT-3710): apply final-review polish
SandipBajracharya b75657d
refactor(OUT-3710): narrow payment scope to event_type='succeeded'
SandipBajracharya bce488c
feat(OUT-3710): add nullable qb_doc_number column to qb_invoice_sync
SandipBajracharya 96be927
chore(OUT-3710): drop superseded oneshot-index migration
SandipBajracharya df5ffbb
feat(OUT-3710): Sentry on walker exhaustion + populate qb_doc_number
SandipBajracharya 71d58d5
fix(OUT-3710): wire payment-succeeded flow to qb_doc_number
SandipBajracharya 01dd7f7
chore(OUT-3720): update query to add column if not exist
SandipBajracharya 673b990
fix(OUT-3710): isQBODuplicateDocNumberError reads errors[].code
SandipBajracharya 89b62b5
test(OUT-3710): align exhaustion test with MAX_SUFFIX_ATTEMPTS constant
SandipBajracharya da60bab
fix(OUT-3710): align with OUT-3543's tightened envelope schemas
SandipBajracharya bd9d5f4
chore(OUT-3710): fix lint
SandipBajracharya e836e62
fix(OUT-3710): include SyncToken while fetching invoices
SandipBajracharya File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| export const formatAssemblyInvoicePrivateNote = ( | ||
| invoiceNumber: string, | ||
| ): string => `Assembly invoice: ${invoiceNumber}` | ||
|
|
||
| const QBO_DOCNUMBER_MAX_LENGTH = 21 | ||
| export const MAX_SUFFIX_ATTEMPTS = 10 | ||
|
|
||
| /** | ||
| * Given the Assembly invoice number and a set of DocNumbers already taken in | ||
| * the target QBO realm, return the next available DocNumber in the sequence | ||
| * `<base>`, `<base>-1`, `<base>-2`, … Only exact-match candidates count as | ||
| * "taken" — unrelated DocNumbers that merely share the base prefix (returned | ||
| * over-broadly by QBO's LIKE query) are ignored. | ||
| * | ||
| * Throws if the candidate exceeds QBO's 21-char DocNumber limit, or if no | ||
| * free slot is found within MAX_SUFFIX_ATTEMPTS iterations. | ||
| */ | ||
| export const findNextAvailableDocNumber = ( | ||
| base: string, | ||
| taken: ReadonlySet<string>, | ||
| ): string => { | ||
| if (base.length > QBO_DOCNUMBER_MAX_LENGTH) { | ||
| throw new Error( | ||
| `DocNumber "${base}" exceeds 21 char limit; QBO will reject.`, | ||
| ) | ||
| } | ||
| if (!taken.has(base)) return base | ||
| for (let n = 1; n <= MAX_SUFFIX_ATTEMPTS; n++) { | ||
| const candidate = `${base}-${n}` | ||
| if (candidate.length > QBO_DOCNUMBER_MAX_LENGTH) { | ||
| throw new Error( | ||
| `DocNumber "${candidate}" exceeds 21 char limit; cannot suffix further.`, | ||
| ) | ||
| } | ||
| if (!taken.has(candidate)) return candidate | ||
| } | ||
| throw new Error( | ||
| `findNextAvailableDocNumber: no available DocNumber for "${base}" after ${MAX_SUFFIX_ATTEMPTS} attempts.`, | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Recognizes QBO Error 6240 "Duplicate Document Number" across the shapes it | ||
| * surfaces in. | ||
| * | ||
| * Live path: APIError thrown from intuitAPI._createInvoice carries the QBO | ||
| * fault payload in its `errors` array (`{ code: '6240', Detail, Message }`). | ||
| * APIError.status lands as 400 because intuitAPI dereferences | ||
| * `Fault.Error?.code` as if it were an object (the QBO Fault.Error is an | ||
| * array); APIError.message is the boilerplate `#IntuitAPIErrorMessage#…`. | ||
| * So the only reliable signal is iterating `errors[]` and matching `code` | ||
| * or the Detail/Message text. | ||
| * | ||
| * Defense-in-depth: also check top-level .status/.code/.message in case any | ||
| * future call site rethrows the inner fault directly or normalizes the | ||
| * APIError differently. | ||
| */ | ||
| export const isQBODuplicateDocNumberError = (err: unknown): boolean => { | ||
| if (!err || typeof err !== 'object' || Array.isArray(err)) return false | ||
| const e = err as { | ||
| status?: string | number | ||
| code?: string | number | ||
| message?: string | ||
| errors?: unknown | ||
| } | ||
| if (Array.isArray(e.errors)) { | ||
| for (const item of e.errors) { | ||
| if (!item || typeof item !== 'object') continue | ||
| const fault = item as { | ||
| code?: string | number | ||
| Detail?: string | ||
| Message?: string | ||
| } | ||
| if (fault.code === 6240 || fault.code === '6240') return true | ||
| if ( | ||
| /6240|Duplicate Document Number/i.test(fault.Detail ?? '') || | ||
| /6240|Duplicate Document Number/i.test(fault.Message ?? '') | ||
| ) { | ||
| return true | ||
| } | ||
| } | ||
| } | ||
| if (e.status === 6240 || e.status === '6240') return true | ||
| if (e.code === 6240 || e.code === '6240') return true | ||
| return /6240|Duplicate Document Number/i.test(e.message ?? '') | ||
| } | ||
|
SandipBajracharya marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.