Kundeninventar-Nummern kollisionsfrei vergeben
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 45s
Build and Push Docker Images / build-frontend (push) Successful in 21s
Build and Push Docker Images / build-website (push) Successful in 21s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 21s
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 45s
Build and Push Docker Images / build-frontend (push) Successful in 21s
Build and Push Docker Images / build-website (push) Successful in 21s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 21s
This commit is contained in:
@@ -1035,7 +1035,25 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
|
||||
if (config.numberRangeHolder && !body[config.numberRangeHolder]) {
|
||||
const numberRangeResource = resource === "members" ? "customers" : resource
|
||||
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, numberRangeResource)
|
||||
const numberRangeColumn = table[config.numberRangeHolder]
|
||||
const tenantColumn = getTenantColumn(resource, table)
|
||||
const result = await useNextNumberRangeNumber(
|
||||
server,
|
||||
req.user.tenant_id,
|
||||
numberRangeResource,
|
||||
async (candidate, tx) => {
|
||||
const existing = await tx
|
||||
.select({ id: table.id })
|
||||
.from(table)
|
||||
.where(and(
|
||||
eq(tenantColumn, req.user!.tenant_id),
|
||||
eq(numberRangeColumn, candidate)
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
return existing.length === 0
|
||||
}
|
||||
)
|
||||
createData[config.numberRangeHolder] = result.usedNumber
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import { eq, sql } from "drizzle-orm"
|
||||
export const useNextNumberRangeNumber = async (
|
||||
server: FastifyInstance,
|
||||
tenantId: number,
|
||||
numberRange: string
|
||||
numberRange: string,
|
||||
isAvailable: (candidate: string, tx: any) => Promise<boolean> = async () => true
|
||||
) => {
|
||||
const numberRangeFallbacks: Record<string, string> = {
|
||||
costEstimates: "quotes",
|
||||
@@ -44,18 +45,39 @@ export const useNextNumberRangeNumber = async (
|
||||
}
|
||||
|
||||
const current = numberRanges[resolvedNumberRange]
|
||||
let nextNumber = Number(current.nextNumber)
|
||||
|
||||
const usedNumber =
|
||||
(current.prefix || "") +
|
||||
current.nextNumber +
|
||||
(current.suffix || "")
|
||||
if (!Number.isFinite(nextNumber)) {
|
||||
throw new Error(`Number range '${resolvedNumberRange}' has an invalid nextNumber`)
|
||||
}
|
||||
|
||||
let usedNumber = ""
|
||||
const maxAttempts = 10000
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const candidate =
|
||||
(current.prefix || "") +
|
||||
nextNumber +
|
||||
(current.suffix || "")
|
||||
|
||||
if (await isAvailable(candidate, tx)) {
|
||||
usedNumber = candidate
|
||||
break
|
||||
}
|
||||
|
||||
nextNumber++
|
||||
}
|
||||
|
||||
if (!usedNumber) {
|
||||
throw new Error(`Could not find an available number in range '${resolvedNumberRange}'`)
|
||||
}
|
||||
|
||||
const updatedRanges = {
|
||||
// @ts-ignore
|
||||
...numberRanges,
|
||||
[resolvedNumberRange]: {
|
||||
...current,
|
||||
nextNumber: current.nextNumber + 1,
|
||||
nextNumber: nextNumber + 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
50
backend/tests/numberRangeAvailability.test.ts
Normal file
50
backend/tests/numberRangeAvailability.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
|
||||
import { useNextNumberRangeNumber } from "../src/utils/functions"
|
||||
|
||||
test("number range skips identifiers that are already in use", async () => {
|
||||
const tenant = {
|
||||
id: 7,
|
||||
numberRanges: {
|
||||
customerinventoryitems: {
|
||||
prefix: "KIA-",
|
||||
suffix: "",
|
||||
nextNumber: 1000,
|
||||
},
|
||||
},
|
||||
}
|
||||
let savedNumberRanges: any = null
|
||||
|
||||
const tx = {
|
||||
execute: async () => undefined,
|
||||
select: () => ({
|
||||
from: () => ({
|
||||
where: async () => [tenant],
|
||||
}),
|
||||
}),
|
||||
update: () => ({
|
||||
set: (data: any) => ({
|
||||
where: async () => {
|
||||
savedNumberRanges = data.numberRanges
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}
|
||||
const server = {
|
||||
db: {
|
||||
transaction: async (callback: (transaction: typeof tx) => Promise<any>) => callback(tx),
|
||||
},
|
||||
} as any
|
||||
const occupied = new Set(["KIA-1000", "KIA-1001"])
|
||||
|
||||
const result = await useNextNumberRangeNumber(
|
||||
server,
|
||||
tenant.id,
|
||||
"customerinventoryitems",
|
||||
async (candidate) => !occupied.has(candidate)
|
||||
)
|
||||
|
||||
assert.equal(result.usedNumber, "KIA-1002")
|
||||
assert.equal(savedNumberRanges.customerinventoryitems.nextNumber, 1003)
|
||||
})
|
||||
Reference in New Issue
Block a user