diff --git a/opsr/backend/activity-log.ts b/opsr/backend/activity-log.ts new file mode 100644 index 00000000..79e6948f --- /dev/null +++ b/opsr/backend/activity-log.ts @@ -0,0 +1,60 @@ +// BE-18: User activity log endpoint + +type ActivityAction = + | "LOGIN" + | "LOGOUT" + | "PASSWORD_CHANGE" + | "SHIPMENT_STATE_CHANGE"; + +interface ActivityLog { + id: string; + userId: string; + action: ActivityAction; + ipAddress: string; + userAgent: string; + createdAt: Date; +} + +interface PaginationOptions { + page: number; + pageSize: number; +} + +const logs: ActivityLog[] = []; + +function generateId(): string { + return Math.random().toString(36).slice(2, 10); +} + +export function logActivity( + userId: string, + action: ActivityAction, + ipAddress: string, + userAgent: string +): ActivityLog { + const entry: ActivityLog = { + id: generateId(), + userId, + action, + ipAddress, + userAgent, + createdAt: new Date(), + }; + logs.push(entry); + return entry; +} + +export function getUserActivity( + userId: string, + { page, pageSize }: PaginationOptions +): { data: ActivityLog[]; total: number } { + const userLogs = logs + .filter((l) => l.userId === userId) + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + + const start = (page - 1) * pageSize; + return { + data: userLogs.slice(start, start + pageSize), + total: userLogs.length, + }; +} diff --git a/opsr/backend/announcement.ts b/opsr/backend/announcement.ts new file mode 100644 index 00000000..b642eca5 --- /dev/null +++ b/opsr/backend/announcement.ts @@ -0,0 +1,64 @@ +// BE-17: Platform-wide announcement system for admins + +interface Announcement { + id: string; + title: string; + body: string; + isActive: boolean; + expiresAt: Date | null; + createdBy: string; + createdAt: Date; +} + +const store: Announcement[] = []; + +function generateId(): string { + return Math.random().toString(36).slice(2, 10); +} + +export function createAnnouncement( + title: string, + body: string, + createdBy: string, + expiresAt?: Date +): Announcement { + const announcement: Announcement = { + id: generateId(), + title, + body, + isActive: true, + expiresAt: expiresAt ?? null, + createdBy, + createdAt: new Date(), + }; + store.push(announcement); + return announcement; +} + +export function getActiveAnnouncements(): Announcement[] { + const now = new Date(); + return store.filter( + (a) => a.isActive && (a.expiresAt === null || a.expiresAt > now) + ); +} + +export function updateAnnouncement( + id: string, + patch: Partial> +): Announcement | null { + const item = store.find((a) => a.id === id); + if (!item) return null; + Object.assign(item, patch); + return item; +} + +export function deleteAnnouncement(id: string): boolean { + const idx = store.findIndex((a) => a.id === id); + if (idx === -1) return false; + store.splice(idx, 1); + return true; +} + +export function getAllAnnouncements(): Announcement[] { + return [...store]; +} diff --git a/opsr/backend/referral.ts b/opsr/backend/referral.ts new file mode 100644 index 00000000..32bd594e --- /dev/null +++ b/opsr/backend/referral.ts @@ -0,0 +1,53 @@ +// BE-19: Referral code system for user registration + +interface User { + id: string; + email: string; + referralCode: string; + referredById: string | null; +} + +const users: User[] = []; + +function generateReferralCode(): string { + return Math.random().toString(36).slice(2, 8).toUpperCase(); +} + +function generateId(): string { + return Math.random().toString(36).slice(2, 10); +} + +export function registerUser( + email: string, + referralCode?: string +): User | { error: string } { + const referrer = referralCode + ? users.find((u) => u.referralCode === referralCode) + : null; + + if (referralCode && !referrer) { + return { error: "Invalid referral code" }; + } + + const newUser: User = { + id: generateId(), + email, + referralCode: generateReferralCode(), + referredById: referrer?.id ?? null, + }; + + users.push(newUser); + return newUser; +} + +export function getUserReferrals(userId: string): User[] { + return users.filter((u) => u.referredById === userId); +} + +export function getUserByReferralCode(code: string): User | undefined { + return users.find((u) => u.referralCode === code); +} + +export function getAllUsers(): User[] { + return [...users]; +} diff --git a/opsr/backend/shipment-cost-calculator.ts b/opsr/backend/shipment-cost-calculator.ts new file mode 100644 index 00000000..f6dc361c --- /dev/null +++ b/opsr/backend/shipment-cost-calculator.ts @@ -0,0 +1,58 @@ +// BE-20: Shipment cost calculator endpoint + +type CargoType = "standard" | "fragile" | "hazardous" | "perishable"; + +interface CostInput { + origin: string; + destination: string; + weightKg: number; + volumeCbm: number; + cargoType: CargoType; +} + +interface CostBreakdown { + baseRate: number; + weightSurcharge: number; + volumeSurcharge: number; + cargoSurcharge: number; + total: number; +} + +const BASE_RATE = 50; +const WEIGHT_RATE = 1.2; // per kg +const VOLUME_RATE = 8; // per cbm + +const CARGO_MULTIPLIERS: Record = { + standard: 1.0, + fragile: 1.3, + hazardous: 1.8, + perishable: 1.5, +}; + +// Naive distance proxy: hash origin+destination into a 100–2000 km range +function estimateDistance(origin: string, destination: string): number { + const hash = [...(origin + destination)].reduce( + (acc, c) => acc + c.charCodeAt(0), + 0 + ); + return 100 + (hash % 1900); +} + +export function calculateShipmentCost(input: CostInput): CostBreakdown { + const { origin, destination, weightKg, volumeCbm, cargoType } = input; + + const distanceFactor = estimateDistance(origin, destination) / 1000; + const baseRate = BASE_RATE * distanceFactor; + const weightSurcharge = weightKg * WEIGHT_RATE * distanceFactor; + const volumeSurcharge = volumeCbm * VOLUME_RATE * distanceFactor; + const subtotal = baseRate + weightSurcharge + volumeSurcharge; + const cargoSurcharge = subtotal * (CARGO_MULTIPLIERS[cargoType] - 1); + + return { + baseRate: +baseRate.toFixed(2), + weightSurcharge: +weightSurcharge.toFixed(2), + volumeSurcharge: +volumeSurcharge.toFixed(2), + cargoSurcharge: +cargoSurcharge.toFixed(2), + total: +(subtotal + cargoSurcharge).toFixed(2), + }; +}