feat(notify): push banking move and shipment arrival owner notifications
This commit is contained in:
parent
2a457a5dd9
commit
bc1456db88
2 changed files with 116 additions and 80 deletions
|
|
@ -13,6 +13,7 @@ import {
|
|||
ensurePersonalAccount,
|
||||
getMainCurrencyName,
|
||||
} from "@/lib/banking";
|
||||
import { notifyUser } from "@/lib/notifications";
|
||||
|
||||
interface ActionResult<T = undefined> {
|
||||
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<number, number | null>();
|
||||
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
export async function processShipmentTick(payload: Payload, shipment: ShipmentDoc): Promise<void> {
|
||||
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,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue