diff --git a/backend/src/app.js b/backend/src/app.js index c900698a..b3d554ea 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -42,8 +42,7 @@ import contractRoutes from "./routes/contractRoutes.js"; import testScreeningRoutes from "./routes/testScreeningRoutes.js"; import recordsRoutes from "./routes/recordsRoutes.js"; import insuranceRoutes from "./routes/insuranceRoutes.js"; -import territoryRoutes from "./routes/territoryRoutes.js"; -import boxOfficeRoutes from "./routes/boxOfficeRoutes.js"; +import agencyRoutes from "./routes/agencyRoutes.js"; const app = express(); @@ -137,8 +136,7 @@ app.use("/api/contracts", apiRateLimiter, contractRoutes); app.use("/api/movies", apiRateLimiter, testScreeningRoutes); app.use("/api/records", apiRateLimiter, recordsRoutes); app.use("/api/insurance", apiRateLimiter, insuranceRoutes); -app.use("/api/territories", apiRateLimiter, territoryRoutes); -app.use("/api/box-office", apiRateLimiter, boxOfficeRoutes); +app.use("/api/talent-agencies", apiRateLimiter, agencyRoutes); app.use((req, res) => { diff --git a/backend/src/controllers/agencyController.js b/backend/src/controllers/agencyController.js new file mode 100644 index 00000000..74daf9ae --- /dev/null +++ b/backend/src/controllers/agencyController.js @@ -0,0 +1,51 @@ +import TalentAgency from "../models/TalentAgency.js"; +import Studio from "../models/Studio.js"; +import { calculatePackageDiscount, evaluatePackageCommission } from "../services/simulation/engines/agencyEngine.js"; + +export const getStudioAgencies = async (req, res, next) => { + try { + const agencies = await TalentAgency.find({ studioId: req.user.studioId }); + return res.status(200).json({ success: true, data: agencies }); + } catch (error) { + next(error); + } +}; + +export const signAgencyPackage = async (req, res, next) => { + try { + const { agencyName, packageValue, talentCount } = req.body; + + let agency = await TalentAgency.findOne({ studioId: req.user.studioId, agencyName }); + if (!agency) { + agency = await TalentAgency.create({ + studioId: req.user.studioId, + agencyName, + relationshipScore: 50, + }); + } + + const discountInfo = calculatePackageDiscount(agency.relationshipScore, talentCount); + const costInfo = evaluatePackageCommission(packageValue, discountInfo.discountPercentage); + + const studio = await Studio.findById(req.user.studioId); + if (studio.money < costInfo.finalPrice) { + return res.status(400).json({ success: false, message: "Insufficient studio funds for talent agency package deal" }); + } + + studio.money -= costInfo.finalPrice; + await studio.save(); + + agency.packagedDealsCount += 1; + agency.relationshipScore = Math.min(100, agency.relationshipScore + 5); + agency.tier = discountInfo.relationshipTier; + await agency.save(); + + return res.status(201).json({ + success: true, + message: `Talent package successfully signed with ${agencyName}`, + data: { agency, costInfo, discountInfo }, + }); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/models/TalentAgency.js b/backend/src/models/TalentAgency.js new file mode 100644 index 00000000..242dc989 --- /dev/null +++ b/backend/src/models/TalentAgency.js @@ -0,0 +1,36 @@ +import mongoose from "mongoose"; + +const talentAgencySchema = new mongoose.Schema( + { + studioId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Studio", + required: true, + index: true, + }, + agencyName: { + type: String, + required: true, + enum: ["Creative Artists Agency", "William Morris Endeavor", "United Talent Agency", "Gersh Agency"], + }, + relationshipScore: { + type: Number, + default: 50, + min: 0, + max: 100, + }, + tier: { + type: String, + enum: ["PREFERRED", "STANDARD", "RESTRICTED"], + default: "STANDARD", + }, + packagedDealsCount: { + type: Number, + default: 0, + }, + }, + { timestamps: true } +); + +const TalentAgency = mongoose.model("TalentAgency", talentAgencySchema); +export default TalentAgency; diff --git a/backend/src/routes/agencyRoutes.js b/backend/src/routes/agencyRoutes.js new file mode 100644 index 00000000..3c9db9f1 --- /dev/null +++ b/backend/src/routes/agencyRoutes.js @@ -0,0 +1,14 @@ +import express from "express"; +import { protect } from "../middleware/authMiddleware.js"; +import validateRequest from "../middleware/validationMiddleware.js"; +import { getStudioAgencies, signAgencyPackage } from "../controllers/agencyController.js"; +import { signAgencyPackageSchema } from "../validators/agencyValidators.js"; + +const router = express.Router(); + +router.use(protect); + +router.get("/agencies", getStudioAgencies); +router.post("/package", validateRequest(signAgencyPackageSchema), signAgencyPackage); + +export default router; diff --git a/backend/src/services/simulation/engines/agencyEngine.js b/backend/src/services/simulation/engines/agencyEngine.js new file mode 100644 index 00000000..5177b68e --- /dev/null +++ b/backend/src/services/simulation/engines/agencyEngine.js @@ -0,0 +1,28 @@ +/** + * Agency Engine + * Evaluates talent agency relationship scores, package discount rates, and agency perks. + */ + +export function calculatePackageDiscount(relationshipScore = 50, talentCount = 3) { + const baseDiscount = 0.05; // 5% base discount for agency packages + const relationshipBonus = (relationshipScore / 100) * 0.10; // up to 10% + const bulkBonus = talentCount >= 3 ? 0.05 : 0; // 5% for 3+ talent bundle + + const totalDiscount = Math.min(0.25, baseDiscount + relationshipBonus + bulkBonus); + + return { + discountPercentage: Math.round(totalDiscount * 100), + relationshipTier: relationshipScore >= 80 ? "PREFERRED" : relationshipScore >= 40 ? "STANDARD" : "RESTRICTED", + }; +} + +export function evaluatePackageCommission(packageValue, discountPct) { + const finalPrice = Math.round(packageValue * (1 - discountPct / 100)); + const agencyCommission = Math.round(finalPrice * 0.10); + + return { + originalValue: packageValue, + finalPrice, + agencyCommission, + }; +} diff --git a/backend/src/validators/agencyValidators.js b/backend/src/validators/agencyValidators.js new file mode 100644 index 00000000..bd3df4ba --- /dev/null +++ b/backend/src/validators/agencyValidators.js @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const signAgencyPackageSchema = z.object({ + agencyName: z.enum(["Creative Artists Agency", "William Morris Endeavor", "United Talent Agency", "Gersh Agency"]), + packageValue: z.number().min(100000, "Minimum package value $100k"), + talentCount: z.number().min(2).max(5).default(3), +}); diff --git a/backend/tests/agencyEngine.test.js b/backend/tests/agencyEngine.test.js new file mode 100644 index 00000000..3bca6d59 --- /dev/null +++ b/backend/tests/agencyEngine.test.js @@ -0,0 +1,19 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { calculatePackageDiscount, evaluatePackageCommission } from "../src/services/simulation/engines/agencyEngine.js"; + +describe("Agency Engine Unit Tests", () => { + it("calculatePackageDiscount returns valid discount pct and tier", () => { + const result = calculatePackageDiscount(85, 3); + + assert.strictEqual(result.relationshipTier, "PREFERRED"); + assert.ok(result.discountPercentage > 15); + }); + + it("evaluatePackageCommission applies discount correctly to package price", () => { + const costInfo = evaluatePackageCommission(1000000, 20); + + assert.strictEqual(costInfo.finalPrice, 800000); + assert.strictEqual(costInfo.agencyCommission, 80000); + }); +}); diff --git a/docs/talent-agency-packaging-system.md b/docs/talent-agency-packaging-system.md new file mode 100644 index 00000000..e673d6d1 --- /dev/null +++ b/docs/talent-agency-packaging-system.md @@ -0,0 +1,16 @@ +# Talent Agency Relations & Executive Packaging System + +## Overview +The Talent Agency Relations & Packaging system allows movie studios to partner with Hollywood's premier talent agencies (CAA, WME, UTA, Gersh) to sign bundled talent packages (Director + Star Actors + Screenwriters) at discounted commission rates. + +## Key Features +1. **Agency Relationship Score**: Building strong standing unlocks package discounts up to 25%. +2. **Talent Packages**: Hiring bundled packages reduces negotiations and streamlines pre-production assembly. +3. **Agency Tiers**: + - `PREFERRED`: High-trust relationship (80+ score). Max packaging discounts. + - `STANDARD`: Normal industry relationship. + - `RESTRICTED`: Strained relationship due to contract breaches or unpaid fees. + +## API Endpoints +- `GET /api/talent-agencies/agencies`: Fetch studio agency relationship standings. +- `POST /api/talent-agencies/package`: Negotiate and sign a talent package deal. diff --git a/frontend/src/pages/talent/AgencyPackagingHub.jsx b/frontend/src/pages/talent/AgencyPackagingHub.jsx new file mode 100644 index 00000000..d95edf3f --- /dev/null +++ b/frontend/src/pages/talent/AgencyPackagingHub.jsx @@ -0,0 +1,59 @@ +import React, { useState, useEffect } from "react"; +import api from "../../api/apiClient"; + +const AgencyPackagingHub = () => { + const [agencies, setAgencies] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchAgencies(); + }, []); + + const fetchAgencies = async () => { + try { + setLoading(true); + const res = await api.get("/talent-agencies/agencies"); + if (res.data.success) { + setAgencies(res.data.data); + } + } catch (err) { + console.error("Failed to load agency standings", err); + } finally { + setLoading(false); + } + }; + + return ( +
Partner with Hollywood talent agencies to negotiate star-studded package deals.
+ + {loading ? ( +No active agency relationship history yet recorded.
+Relationship Score: {a.relationshipScore} / 100
+Packages Executed: {a.packagedDealsCount}
+