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]) {
|
if (config.numberRangeHolder && !body[config.numberRangeHolder]) {
|
||||||
const numberRangeResource = resource === "members" ? "customers" : resource
|
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
|
createData[config.numberRangeHolder] = result.usedNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { eq, sql } from "drizzle-orm"
|
|||||||
export const useNextNumberRangeNumber = async (
|
export const useNextNumberRangeNumber = async (
|
||||||
server: FastifyInstance,
|
server: FastifyInstance,
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
numberRange: string
|
numberRange: string,
|
||||||
|
isAvailable: (candidate: string, tx: any) => Promise<boolean> = async () => true
|
||||||
) => {
|
) => {
|
||||||
const numberRangeFallbacks: Record<string, string> = {
|
const numberRangeFallbacks: Record<string, string> = {
|
||||||
costEstimates: "quotes",
|
costEstimates: "quotes",
|
||||||
@@ -44,18 +45,39 @@ export const useNextNumberRangeNumber = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const current = numberRanges[resolvedNumberRange]
|
const current = numberRanges[resolvedNumberRange]
|
||||||
|
let nextNumber = Number(current.nextNumber)
|
||||||
|
|
||||||
const usedNumber =
|
if (!Number.isFinite(nextNumber)) {
|
||||||
(current.prefix || "") +
|
throw new Error(`Number range '${resolvedNumberRange}' has an invalid nextNumber`)
|
||||||
current.nextNumber +
|
}
|
||||||
(current.suffix || "")
|
|
||||||
|
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 = {
|
const updatedRanges = {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
...numberRanges,
|
...numberRanges,
|
||||||
[resolvedNumberRange]: {
|
[resolvedNumberRange]: {
|
||||||
...current,
|
...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