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
6 changes: 2 additions & 4 deletions backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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) => {
Expand Down
51 changes: 51 additions & 0 deletions backend/src/controllers/agencyController.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
36 changes: 36 additions & 0 deletions backend/src/models/TalentAgency.js
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 14 additions & 0 deletions backend/src/routes/agencyRoutes.js
Original file line number Diff line number Diff line change
@@ -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;
28 changes: 28 additions & 0 deletions backend/src/services/simulation/engines/agencyEngine.js
Original file line number Diff line number Diff line change
@@ -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,
};
}
7 changes: 7 additions & 0 deletions backend/src/validators/agencyValidators.js
Original file line number Diff line number Diff line change
@@ -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),
});
19 changes: 19 additions & 0 deletions backend/tests/agencyEngine.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
16 changes: 16 additions & 0 deletions docs/talent-agency-packaging-system.md
Original file line number Diff line number Diff line change
@@ -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.
59 changes: 59 additions & 0 deletions frontend/src/pages/talent/AgencyPackagingHub.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="p-6 max-w-6xl mx-auto">
<h1 className="text-3xl font-bold text-white mb-2">Talent Agency Executive Packaging</h1>
<p className="text-gray-400 mb-6">Partner with Hollywood talent agencies to negotiate star-studded package deals.</p>

{loading ? (
<div className="text-gray-400">Loading agency relationship profiles...</div>
) : agencies.length === 0 ? (
<div className="bg-gray-800 p-8 rounded-xl text-center border border-gray-700">
<p className="text-gray-400">No active agency relationship history yet recorded.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{agencies.map((a) => (
<div key={a._id} className="bg-gray-800 p-5 rounded-xl border border-gray-700">
<div className="flex justify-between items-center mb-3">
<h3 className="font-bold text-lg text-white">{a.agencyName}</h3>
<span className={`text-xs px-2.5 py-1 rounded font-bold ${a.tier === "PREFERRED" ? "bg-purple-900 text-purple-300" : "bg-gray-700 text-gray-300"}`}>
{a.tier}
</span>
</div>
<div className="text-sm space-y-1 text-gray-300">
<p>Relationship Score: <span className="text-indigo-400 font-semibold">{a.relationshipScore} / 100</span></p>
<p>Packages Executed: <span className="text-green-400 font-semibold">{a.packagedDealsCount}</span></p>
</div>
</div>
))}
</div>
)}
</div>
);
};

export default AgencyPackagingHub;