From bc1456db888ea89c437157c4ea55747416bebb00 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Wed, 12 Aug 2026 12:38:46 -0400 Subject: [PATCH] feat(notify): push banking move and shipment arrival owner notifications --- .../(frontend)/logistics/banking/actions.ts | 81 ++++++++++-- src/scripts/processShipmentTick.ts | 115 +++++++----------- 2 files changed, 116 insertions(+), 80 deletions(-) diff --git a/src/app/(frontend)/logistics/banking/actions.ts b/src/app/(frontend)/logistics/banking/actions.ts index 9038dc4..87ee51e 100644 --- a/src/app/(frontend)/logistics/banking/actions.ts +++ b/src/app/(frontend)/logistics/banking/actions.ts @@ -13,6 +13,7 @@ import { ensurePersonalAccount, getMainCurrencyName, } from "@/lib/banking"; +import { notifyUser } from "@/lib/notifications"; interface ActionResult { success: boolean; @@ -66,10 +67,7 @@ export async function createBankAccount(input: { const existing = await payload.find({ collection: "bank-accounts", where: { - and: [ - { accountType: { equals: "personal" } }, - { ownerUser: { equals: ownerId } }, - ], + and: [{ accountType: { equals: "personal" } }, { ownerUser: { equals: ownerId } }], }, limit: 1, depth: 0, @@ -100,8 +98,7 @@ export async function createBankAccount(input: { if (!manager) { return { success: false, - error: - "Insufficient permissions. Logistics personnel or higher can manage unit accounts.", + error: "Insufficient permissions. Logistics personnel or higher can manage unit accounts.", }; } @@ -204,6 +201,8 @@ async function moveFunds( if (fromAccountId) accountChecks.push({ id: fromAccountId, label: "source" }); if (toAccountId) accountChecks.push({ id: toAccountId, label: "destination" }); + const accountOwners = new Map(); + for (const check of accountChecks) { const account = (await payload .findByID({ @@ -224,11 +223,10 @@ async function moveFunds( if (account.status === "closed") { return { success: false, error: `${check.label} account is closed.` }; } + const ownerId = + typeof account.ownerUser === "object" ? account.ownerUser?.id : account.ownerUser; + accountOwners.set(check.id as number, ownerId ?? null); if (account.accountType === "personal") { - const ownerId = - typeof account.ownerUser === "object" - ? account.ownerUser?.id - : account.ownerUser; if (!manager && ownerId !== user.id) { return { success: false, @@ -238,8 +236,7 @@ async function moveFunds( } else if (!manager) { return { success: false, - error: - "Insufficient permissions. Logistics personnel or higher can move unit funds.", + error: "Insufficient permissions. Logistics personnel or higher can move unit funds.", }; } } @@ -293,6 +290,66 @@ async function moveFunds( data: { fromAccountId, toAccountId, amount, type }, }); + const notifications: { userId: number; message: string; link: string }[] = []; + if (type === "deposit" && toAccountId) { + const ownerId = accountOwners.get(toAccountId) ?? null; + if (ownerId != null) { + notifications.push({ + userId: ownerId, + message: `Deposited ${amountLabel} into your account (#${toAccountId}).`, + link: `/logistics/banking/${toAccountId}`, + }); + } + } else if (type === "withdrawal" && fromAccountId) { + const ownerId = accountOwners.get(fromAccountId) ?? null; + if (ownerId != null) { + notifications.push({ + userId: ownerId, + message: `Withdrew ${amountLabel} from your account (#${fromAccountId}).`, + link: `/logistics/banking/${fromAccountId}`, + }); + } + } else if (type === "transfer" && fromAccountId && toAccountId) { + const fromOwner = accountOwners.get(fromAccountId) ?? null; + const toOwner = accountOwners.get(toAccountId) ?? null; + if (fromOwner != null) { + notifications.push({ + userId: fromOwner, + message: `Transferred ${amountLabel} from your account (#${fromAccountId}) to account #${toAccountId}.`, + link: `/logistics/banking/${fromAccountId}`, + }); + } + if (toOwner != null && toOwner !== fromOwner) { + notifications.push({ + userId: toOwner, + message: `Received ${amountLabel} in account #${toAccountId} from account #${fromAccountId}.`, + link: `/logistics/banking/${toAccountId}`, + }); + } + } + + const notifType = + type === "deposit" + ? "finance:deposit" + : type === "withdrawal" + ? "finance:withdraw" + : "finance:transfer"; + const notifTitle = + type === "deposit" + ? "Bank deposit" + : type === "withdrawal" + ? "Bank withdrawal" + : "Bank transfer"; + for (const n of notifications) { + await notifyUser(payload, { + userId: n.userId, + type: notifType, + title: notifTitle, + message: n.message, + link: n.link, + }); + } + return { success: true, data: transaction.id }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; diff --git a/src/scripts/processShipmentTick.ts b/src/scripts/processShipmentTick.ts index db9af30..1aff253 100644 --- a/src/scripts/processShipmentTick.ts +++ b/src/scripts/processShipmentTick.ts @@ -2,10 +2,8 @@ import type { Payload } from "payload"; import type { Shipment, GameVehicle, GameStructure, Vehicle } from "@/payload-types"; import { emitGameEvent } from "@/utils/event-log/emit"; import { EventTypes } from "@/utils/event-log/eventTypes"; -import { - checkStorageDeposit, - storageViolationMessage, -} from "@/lib/storageRules"; +import { notifyUser } from "@/lib/notifications"; +import { checkStorageDeposit, storageViolationMessage } from "@/lib/storageRules"; type ShipmentDoc = Shipment & { transportVehicle: GameVehicle & { type: Vehicle }; @@ -13,10 +11,7 @@ type ShipmentDoc = Shipment & { destination: GameStructure; }; -export async function processShipmentTick( - payload: Payload, - shipment: ShipmentDoc, -): Promise { +export async function processShipmentTick(payload: Payload, shipment: ShipmentDoc): Promise { const now = new Date(); const vehicle = shipment.transportVehicle; @@ -28,14 +23,9 @@ export async function processShipmentTick( const dispatchedAt = new Date(shipment.dispatchedAt!); const estimatedArrival = new Date(shipment.estimatedArrival!); const gameRules = await payload.findGlobal({ slug: "game-rules" }); - const tickIntervalMinutes = - (gameRules.gameTickIntervalMinutes as number) || 10; - const totalDurationMs = - estimatedArrival.getTime() - dispatchedAt.getTime(); - const totalTicks = Math.max( - 1, - Math.ceil(totalDurationMs / (tickIntervalMinutes * 60 * 1000)), - ); + const tickIntervalMinutes = (gameRules.gameTickIntervalMinutes as number) || 10; + const totalDurationMs = estimatedArrival.getTime() - dispatchedAt.getTime(); + const totalTicks = Math.max(1, Math.ceil(totalDurationMs / (tickIntervalMinutes * 60 * 1000))); const fuelPerTick = shipment.fuelCost / totalTicks; const currentFuelConsumed = shipment.fuelConsumed ?? 0; @@ -57,9 +47,7 @@ export async function processShipmentTick( }); const originId = - typeof shipment.origin === "object" - ? shipment.origin.id - : (shipment.origin as number); + typeof shipment.origin === "object" ? shipment.origin.id : (shipment.origin as number); await emitGameEvent(payload, { type: EventTypes.ShipmentFail, @@ -79,10 +67,7 @@ export async function processShipmentTick( data: { currentFuel: newFuel }, }); - const newFuelConsumed = Math.min( - currentFuelConsumed + fuelPerTick, - shipment.fuelCost, - ); + const newFuelConsumed = Math.min(currentFuelConsumed + fuelPerTick, shipment.fuelCost); if (now >= estimatedArrival) { await completeShipment(payload, shipment, newFuelConsumed); @@ -110,9 +95,7 @@ async function completeShipment( ? shipment.destination.id : (shipment.destination as number); const originId = - typeof shipment.origin === "object" - ? shipment.origin.id - : (shipment.origin as number); + typeof shipment.origin === "object" ? shipment.origin.id : (shipment.origin as number); const bounced: { itemId: number; amount: number; resourceName: string }[] = []; @@ -123,10 +106,7 @@ async function completeShipment( const polymorphic = itemRef as { relationTo: string; value: number | { id: number } }; if (polymorphic.relationTo !== "resources") continue; - const itemId = - typeof polymorphic.value === "object" - ? polymorphic.value.id - : polymorphic.value; + const itemId = typeof polymorphic.value === "object" ? polymorphic.value.id : polymorphic.value; try { const destination = await payload.findByID({ @@ -139,27 +119,20 @@ async function completeShipment( if (destType) { const destStored = destination.storedResources ?? []; const destVoid = (destination as GameStructure).voidStorage ?? []; - const currentStored = [...destStored, ...destVoid].reduce( - (sum, r) => { - const resId = - typeof r.resource === "object" ? r.resource.id : r.resource; - return sum + (resId === itemId ? r.amount : 0); - }, - 0, - ); + const currentStored = [...destStored, ...destVoid].reduce((sum, r) => { + const resId = typeof r.resource === "object" ? r.resource.id : r.resource; + return sum + (resId === itemId ? r.amount : 0); + }, 0); - const ruleCheck = checkStorageDeposit( - destType, - itemId, - cargoEntry.amount, - currentStored, - ); + const ruleCheck = checkStorageDeposit(destType, itemId, cargoEntry.amount, currentStored); if (!ruleCheck.allowed) { const resName = - (await payload - .findByID({ collection: "resources", id: itemId, depth: 0 }) - .catch(() => null))?.name ?? `resource #${itemId}`; + ( + await payload + .findByID({ collection: "resources", id: itemId, depth: 0 }) + .catch(() => null) + )?.name ?? `resource #${itemId}`; bounced.push({ itemId, amount: cargoEntry.amount, @@ -174,8 +147,7 @@ async function completeShipment( const existingResources = destination.storedResources ?? []; const existingEntry = existingResources.find((r) => { - const resId = - typeof r.resource === "object" ? r.resource.id : r.resource; + const resId = typeof r.resource === "object" ? r.resource.id : r.resource; return resId === itemId; }); @@ -224,8 +196,7 @@ async function completeShipment( const existingResources = [...(origin.storedResources ?? [])]; for (const b of bounced) { const existingEntry = existingResources.find((r) => { - const resId = - typeof r.resource === "object" ? r.resource.id : r.resource; + const resId = typeof r.resource === "object" ? r.resource.id : r.resource; return resId === b.itemId; }); if (existingEntry) { @@ -267,7 +238,11 @@ async function completeShipment( targetId: shipment.id, data: { reason: "storage_rules", - bounced: bounced.map((b) => ({ itemId: b.itemId, amount: b.amount, resourceName: b.resourceName })), + bounced: bounced.map((b) => ({ + itemId: b.itemId, + amount: b.amount, + resourceName: b.resourceName, + })), }, }); return; @@ -292,12 +267,23 @@ async function completeShipment( data: { vehicleId: vehicle.id, originId, destinationId }, }); + // Notify the user who created the shipment (the closest thing to a recipient/owner). + const creatorId = + typeof shipment.createdBy === "object" && shipment.createdBy + ? shipment.createdBy.id + : (shipment.createdBy as number | null | undefined); + if (creatorId != null) { + await notifyUser(payload, { + userId: creatorId, + type: "shipment:completed", + title: "Shipment completed", + message: `Shipment "${shipment.name ?? `SHP-${shipment.id}`}" arrived at its destination.`, + link: `/logistics/shipments/${shipment.id}`, + }); + } + if (shipment.autoReturn) { - await createAutoReturnShipment( - payload, - shipment, - vehicle as GameVehicle & { type: Vehicle }, - ); + await createAutoReturnShipment(payload, shipment, vehicle as GameVehicle & { type: Vehicle }); } } @@ -315,20 +301,15 @@ async function createAutoReturnShipment( ? originalShipment.destination.id : (originalShipment.destination as number); - const vehicleType = - typeof vehicle.type === "object" ? vehicle.type : null; + const vehicleType = typeof vehicle.type === "object" ? vehicle.type : null; if (!vehicleType) return; const speed = - vehicleType.maxSpeedOnRoad ?? - vehicleType.maxSpeedOffRoad ?? - vehicleType.maxAirspeed ?? - 50; + vehicleType.maxSpeedOnRoad ?? vehicleType.maxSpeedOffRoad ?? vehicleType.maxAirspeed ?? 50; const rate = vehicleType.fuel?.fuelConsumptionRate ?? 1; const returnFuelCost = Math.ceil(originalShipment.distance * rate); - const returnDurationMs = - (originalShipment.distance / speed) * 60 * 60 * 1000; + const returnDurationMs = (originalShipment.distance / speed) * 60 * 60 * 1000; const now = new Date(); if (vehicle.currentFuel < returnFuelCost) { @@ -366,9 +347,7 @@ async function createAutoReturnShipment( fuelConsumed: 0, status: "dispatched", dispatchedAt: now.toISOString(), - estimatedArrival: new Date( - now.getTime() + returnDurationMs, - ).toISOString(), + estimatedArrival: new Date(now.getTime() + returnDurationMs).toISOString(), autoReturn: false, createdBy: originalShipment.createdBy, },