Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions opsr/backend/activity-log.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
64 changes: 64 additions & 0 deletions opsr/backend/announcement.ts
Original file line number Diff line number Diff line change
@@ -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<Pick<Announcement, "title" | "body" | "isActive" | "expiresAt">>
): 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];
}
53 changes: 53 additions & 0 deletions opsr/backend/referral.ts
Original file line number Diff line number Diff line change
@@ -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];
}
58 changes: 58 additions & 0 deletions opsr/backend/shipment-cost-calculator.ts
Original file line number Diff line number Diff line change
@@ -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<CargoType, number> = {
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),
};
}
Loading