diff --git a/apps/backend/src/repositories/worker-platform-repo.test.ts b/apps/backend/src/repositories/worker-platform-repo.test.ts index 17669e2f..90c07db8 100644 --- a/apps/backend/src/repositories/worker-platform-repo.test.ts +++ b/apps/backend/src/repositories/worker-platform-repo.test.ts @@ -1,7 +1,10 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { resolveOutstandingDepositAmount } from './worker-platform/index.js' +import { + resolveOutstandingDepositAmount, + shouldApplyAutomaticWorkerLevel, +} from './worker-platform/index.js' test('resolveOutstandingDepositAmount deducts released and deducted history', () => { assert.equal( @@ -25,3 +28,33 @@ test('resolveOutstandingDepositAmount never returns a negative deposit', () => { ) assert.equal(resolveOutstandingDepositAmount(null), 0) }) + +test('automatic worker level changes only move to a higher threshold', () => { + assert.equal( + shouldApplyAutomaticWorkerLevel({ + currentLevelId: 5, + currentThreshold: 100, + candidateLevelId: 1, + candidateThreshold: 0, + }), + false, + ) + assert.equal( + shouldApplyAutomaticWorkerLevel({ + currentLevelId: 1, + currentThreshold: 0, + candidateLevelId: 2, + candidateThreshold: 10, + }), + true, + ) + assert.equal( + shouldApplyAutomaticWorkerLevel({ + currentLevelId: 2, + currentThreshold: 10, + candidateLevelId: 3, + candidateThreshold: 10, + }), + false, + ) +}) diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index 03006011..d662aa87 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -762,6 +762,27 @@ export async function maybeUpgradeWorkerLevelWithClient( workerId: number, now: string, ): Promise { + const current = await client.query<{ + id: number + level_id: number | null + upgrade_threshold: number | null + }>( + ` + SELECT + wu.id, + wu.level_id, + COALESCE((wl.permission_json->>'upgradeThreshold')::int, 0) AS upgrade_threshold + FROM worker_users wu + LEFT JOIN worker_levels wl ON wl.id = wu.level_id + WHERE wu.id = $1 + LIMIT 1 + FOR UPDATE OF wu + `, + [workerId], + ) + const currentRow = current.rows[0] + if (!currentRow) return null + const acceptedResult = await client.query<{ total: number }>( ` SELECT @@ -776,9 +797,14 @@ export async function maybeUpgradeWorkerLevelWithClient( ) const acceptedCount = Number(acceptedResult.rows[0]?.total || 0) - const nextLevelResult = await client.query<{ id: number }>( + const nextLevelResult = await client.query<{ + id: number + upgrade_threshold: number + }>( ` - SELECT id + SELECT + id, + COALESCE((permission_json->>'upgradeThreshold')::int, 0) AS upgrade_threshold FROM worker_levels WHERE status = 'active' AND COALESCE((permission_json->>'upgradeThreshold')::int, 0) <= $1 @@ -787,15 +813,19 @@ export async function maybeUpgradeWorkerLevelWithClient( `, [acceptedCount], ) - const nextLevelId = Number(nextLevelResult.rows[0]?.id || 0) + const nextLevel = nextLevelResult.rows[0] + const nextLevelId = Number(nextLevel?.id || 0) if (!nextLevelId) return null - - const current = await client.query<{ id: number; level_id: number | null }>( - 'SELECT id, level_id FROM worker_users WHERE id = $1 LIMIT 1', - [workerId], - ) - const currentRow = current.rows[0] - if (!currentRow || Number(currentRow.level_id || 0) === nextLevelId) return null + if ( + !shouldApplyAutomaticWorkerLevel({ + currentLevelId: currentRow.level_id, + currentThreshold: Number(currentRow.upgrade_threshold || 0), + candidateLevelId: nextLevelId, + candidateThreshold: Number(nextLevel?.upgrade_threshold || 0), + }) + ) { + return null + } await client.query( 'UPDATE worker_users SET level_id = $1, updated_at = $2 WHERE id = $3', @@ -818,6 +848,17 @@ export async function maybeUpgradeWorkerLevelWithClient( return getWorkerUserByIdWithClient(client, workerId) } +export function shouldApplyAutomaticWorkerLevel(input: { + currentLevelId: number | null + currentThreshold: number + candidateLevelId: number + candidateThreshold: number +}) { + if (!input.currentLevelId) return true + if (Number(input.currentLevelId) === Number(input.candidateLevelId)) return false + return Number(input.candidateThreshold) > Number(input.currentThreshold) +} + async function getWorkerUserByIdWithClient( client: PoolClient, workerId: number | string, diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 8002f3fd..f6be33d1 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -147,6 +147,7 @@ export async function deleteAdminWorkCategory(categoryId: number | string) { export async function saveAdminWorkerLevel(payload: JsonObject = {}) { const now = nowIso() const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY + const existingLevel = await getWorkerLevelByKey(levelKey) const name = String(payload.name || '').trim() || DEFAULT_LEVEL_NAME const permissions = { depositFreeAmount: normalizeAmountFen( @@ -160,7 +161,7 @@ export async function saveAdminWorkerLevel(payload: JsonObject = {}) { const level = await upsertWorkerLevel({ levelKey, name, - sortOrder: normalizeInteger(payload.sortOrder, 100), + sortOrder: normalizeInteger(payload.sortOrder, Number(existingLevel?.sort_order || 100)), status: String(payload.status || 'active').trim() === 'disabled' ? 'disabled' : 'active', permissionJson: JSON.stringify(permissions), now,