Master legion! Consul Caesar.js announces a grand challenge! It's time to combine all the skills you've acquired in this module and create a complete Roman tribute inventory system. This will be your first real enterprise project - a treasury that meets the demands of the greatest Roman legions!
You will create a complete tribute management system for the Roman legion, which will include:
1// entities/legionaries.entity.ts
2import {
3 Entity,
4 PrimaryGeneratedColumn,
5 Column,
6 OneToMany,
7 ManyToOne,
8 OneToOne,
9 CreateDateColumn,
10 UpdateDateColumn,
11 Check,
12 Index
13} from 'typeorm';
14
15@Entity('legionaries')
16@Check('tribute_count_non_negative', '"tributeCount" >= 0')
17@Check('tribute_value_non_negative', '"totalTributeValue" >= 0')
18@Check('valid_rank', '"rank" IN (\'Tiro\', \'Tesserarius\', \'Speculator\', \'Optio\', \'Centurion\', \'Legate\')')
19@Index(['name', 'isActive'])
20export class Legionary {
21 @PrimaryGeneratedColumn()
22 id: number;
23
24 @Column({ length: 100, unique: true })
25 @Index()
26 name: string;
27
28 @Column({ length: 50 })
29 rank: string;
30
31 @Column({ length: 100, nullable: true })
32 cohort: string;
33
34 @Column({ type: 'int', default: 0 })
35 tributeCount: number;
36
37 @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
38 totalTributeValue: number;
39
40 @Column({ type: 'json', nullable: true })
41 skills: {
42 navigation: number;
43 combat: number;
44 negotiation: number;
45 leadership: number;
46 tributeHunting: number;
47 };
48
49 @Column({ type: 'text', nullable: true })
50 biography: string;
51
52 @Column({ default: true })
53 isActive: boolean;
54
55 @Column({ type: 'decimal', precision: 10, scale: 6, nullable: true })
56 lastKnownLatitude: number;
57
58 @Column({ type: 'decimal', precision: 10, scale: 6, nullable: true })
59 lastKnownLongitude: number;
60
61 @CreateDateColumn()
62 joinedAt: Date;
63
64 @UpdateDateColumn()
65 lastUpdated: Date;
66
67 @Column({ type: 'timestamp', nullable: true })
68 lastSeenAt: Date;
69
70 // Relations
71 @OneToMany(() => Tribute, tribute => tribute.owner)
72 tributes: Tribute[];
73
74 @OneToMany(() => TributeMap, map => map.owner)
75 tributeMaps: TributeMap[];
76
77 @ManyToOne(() => Legionary, legionaries => legionaries.teamMembers)
78 centurion: Legionary;
79
80 @OneToMany(() => Legionary, legionaries => legionaries.centurion)
81 teamMembers: Legionary[];
82
83 @OneToMany(() => Transaction, transaction => transaction.fromLegionary)
84 outgoingTransactions: Transaction[];
85
86 @OneToMany(() => Transaction, transaction => transaction.toLegionary)
87 incomingTransactions: Transaction[];
88}
89
90// entities/tribute.entity.ts
91@Entity('tributes')
92@Check('value_positive', '"value" > 0')
93@Check('value_reasonable', '"value" <= 10000000')
94@Check('valid_type', '"type" IN (\'gold\', \'silver\', \'jewels\', \'artifacts\', \'weapons\', \'maps\')')
95@Check('rarity_valid', '"rarity" IN (\'Common\', \'Uncommon\', \'Rare\', \'Epic\', \'Legendary\')')
96export class Tribute {
97 @PrimaryGeneratedColumn()
98 id: number;
99
100 @Column({ length: 100 })
101 @Index()
102 name: string;
103
104 @Column({ length: 50 })
105 @Index()
106 type: string;
107
108 @Column('decimal', { precision: 15, scale: 2 })
109 @Index()
110 value: number;
111
112 @Column({ length: 50, default: 'Common' })
113 rarity: string;
114
115 @Column({ type: 'text', nullable: true })
116 description: string;
117
118 @Column({ length: 100 })
119 @Index()
120 location: string;
121
122 @Column({ default: false })
123 isCursed: boolean;
124
125 @Column({ default: false })
126 isAuthentic: boolean;
127
128 @Column({ type: 'json', nullable: true })
129 properties: {
130 material?: string;
131 age?: number;
132 origin?: string;
133 previousOwners?: string[];
134 magicalProperties?: string[];
135 };
136
137 @CreateDateColumn()
138 collectedAt: Date;
139
140 @UpdateDateColumn()
141 lastUpdated: Date;
142
143 // Relations
144 @ManyToOne(() => Legionary, legionaries => legionaries.tributes)
145 @Index()
146 owner: Legionary;
147
148 @OneToOne(() => TributeMap, map => map.tribute)
149 tributeMap: TributeMap;
150
151 @OneToMany(() => Transaction, transaction => transaction.tribute)
152 transactions: Transaction[];
153
154 @OneToMany(() => TributeAppraisal, appraisal => appraisal.tribute)
155 appraisals: TributeAppraisal[];
156}
157
158// entities/tribute-map.entity.ts
159@Entity('tribute_maps')
160@Check('valid_coordinates', '"latitude" BETWEEN -90 AND 90 AND "longitude" BETWEEN -180 AND 180')
161export class TributeMap {
162 @PrimaryGeneratedColumn()
163 id: number;
164
165 @Column({ length: 100 })
166 islandName: string;
167
168 @Column('decimal', { precision: 10, scale: 6 })
169 latitude: number;
170
171 @Column('decimal', { precision: 10, scale: 6 })
172 longitude: number;
173
174 @Column('text')
175 instructions: string;
176
177 @Column({ default: false })
178 isFound: boolean;
179
180 @Column({ type: 'int', default: 1 })
181 difficultyLevel: number;
182
183 @Column({ type: 'json', nullable: true })
184 clues: {
185 landmarks: string[];
186 distances: string[];
187 warnings: string[];
188 };
189
190 @CreateDateColumn()
191 createdAt: Date;
192
193 @Column({ type: 'timestamp', nullable: true })
194 foundAt: Date;
195
196 // Relations
197 @ManyToOne(() => Legionary, legionaries => legionaries.tributeMaps)
198 owner: Legionary;
199
200 @OneToOne(() => Tribute, tribute => tribute.tributeMap)
201 @JoinColumn()
202 tribute: Tribute;
203}
204
205// entities/transaction.entity.ts
206@Entity('transactions')
207@Check('valid_type', '"type" IN (\'TRANSFER\', \'TRADE\', \'GIFT\', \'THEFT\', \'PURCHASE\', \'SALE\')')
208@Check('valid_status', '"status" IN (\'PENDING\', \'COMPLETED\', \'FAILED\', \'CANCELLED\')')
209export class Transaction {
210 @PrimaryGeneratedColumn()
211 id: number;
212
213 @Column({ length: 50 })
214 @Index()
215 type: string;
216
217 @Column({ length: 50, default: 'PENDING' })
218 @Index()
219 status: string;
220
221 @Column('decimal', { precision: 15, scale: 2 })
222 value: number;
223
224 @Column({ type: 'text', nullable: true })
225 notes: string;
226
227 @Column({ type: 'json', nullable: true })
228 metadata: {
229 exchangeRate?: number;
230 witnesses?: string[];
231 location?: string;
232 contract?: string;
233 };
234
235 @CreateDateColumn()
236 createdAt: Date;
237
238 @Column({ type: 'timestamp', nullable: true })
239 completedAt: Date;
240
241 // Relations
242 @ManyToOne(() => Legionary, legionaries => legionaries.outgoingTransactions)
243 @Index()
244 fromLegionary: Legionary;
245
246 @ManyToOne(() => Legionary, legionaries => legionaries.incomingTransactions)
247 @Index()
248 toLegionary: Legionary;
249
250 @ManyToOne(() => Tribute, tribute => tribute.transactions)
251 @Index()
252 tribute: Tribute;
253
254 @ManyToOne(() => Tribute, { nullable: true })
255 tradedTribute: Tribute; // For trades
256}
257
258// entities/tribute-appraisal.entity.ts
259@Entity('tribute_appraisals')
260export class TributeAppraisal {
261 @PrimaryGeneratedColumn()
262 id: number;
263
264 @Column('decimal', { precision: 15, scale: 2 })
265 estimatedValue: number;
266
267 @Column('decimal', { precision: 15, scale: 2 })
268 marketValue: number;
269
270 @Column({ length: 100 })
271 appraiserName: string;
272
273 @Column({ type: 'int', default: 1 })
274 confidenceLevel: number; // 1-10
275
276 @Column({ type: 'text' })
277 notes: string;
278
279 @CreateDateColumn()
280 appraisedAt: Date;
281
282 // Relations
283 @ManyToOne(() => Tribute, tribute => tribute.appraisals)
284 tribute: Tribute;
285}1// repositories/tribute.repository.ts
2@EntityRepository(Tribute)
3export class TributeRepository extends Repository<Tribute> {
4 // Advanced tribute search
5 async findAdvanced(criteria: {
6 type?: string[];
7 minValue?: number;
8 maxValue?: number;
9 rarity?: string[];
10 location?: string;
11 isAuthentic?: boolean;
12 excludeCursed?: boolean;
13 ownerId?: number;
14 }): Promise<Tribute[]> {
15 let query = this.createQueryBuilder('tribute')
16 .leftJoinAndSelect('tribute.owner', 'owner')
17 .leftJoinAndSelect('tribute.tributeMap', 'map');
18
19 if (criteria.type && criteria.type.length > 0) {
20 query = query.andWhere('tribute.type IN (:...types)', { types: criteria.type });
21 }
22
23 if (criteria.minValue !== undefined) {
24 query = query.andWhere('tribute.value >= :minValue', { minValue: criteria.minValue });
25 }
26
27 if (criteria.maxValue !== undefined) {
28 query = query.andWhere('tribute.value <= :maxValue', { maxValue: criteria.maxValue });
29 }
30
31 if (criteria.rarity && criteria.rarity.length > 0) {
32 query = query.andWhere('tribute.rarity IN (:...rarities)', { rarities: criteria.rarity });
33 }
34
35 if (criteria.location) {
36 query = query.andWhere('tribute.location ILIKE :location', {
37 location: `%${criteria.location}%`
38 });
39 }
40
41 if (criteria.isAuthentic !== undefined) {
42 query = query.andWhere('tribute.isAuthentic = :isAuthentic', {
43 isAuthentic: criteria.isAuthentic
44 });
45 }
46
47 if (criteria.excludeCursed) {
48 query = query.andWhere('tribute.isCursed = false');
49 }
50
51 if (criteria.ownerId) {
52 query = query.andWhere('tribute.owner.id = :ownerId', { ownerId: criteria.ownerId });
53 }
54
55 return query
56 .orderBy('tribute.value', 'DESC')
57 .getMany();
58 }
59
60 // Tribute market analysis
61 async getMarketAnalysis(): Promise<any[]> {
62 return this.createQueryBuilder('tribute')
63 .select([
64 'tribute.type as type',
65 'tribute.rarity as rarity',
66 'COUNT(*) as count',
67 'AVG(tribute.value) as avgValue',
68 'MIN(tribute.value) as minValue',
69 'MAX(tribute.value) as maxValue',
70 'SUM(tribute.value) as totalValue'
71 ])
72 .where('tribute.isAuthentic = true')
73 .groupBy('tribute.type, tribute.rarity')
74 .orderBy('avgValue', 'DESC')
75 .getRawMany();
76 }
77
78 // Top collectors
79 async getTopCollectors(limit: number = 10): Promise<any[]> {
80 return this.createQueryBuilder('tribute')
81 .leftJoin('tribute.owner', 'owner')
82 .select([
83 'owner.id as legionariesId',
84 'owner.name as userName',
85 'owner.rank as rank',
86 'COUNT(*) as tributeCount',
87 'SUM(tribute.value) as totalValue',
88 'AVG(tribute.value) as avgValue'
89 ])
90 .where('tribute.owner IS NOT NULL')
91 .groupBy('owner.id, owner.name, owner.rank')
92 .orderBy('totalValue', 'DESC')
93 .limit(limit)
94 .getRawMany();
95 }
96
97 // Tributes without an owner
98 async findUnowned(): Promise<Tribute[]> {
99 return this.find({
100 where: { owner: null },
101 order: { value: 'DESC' }
102 });
103 }
104
105 // Location statistics
106 async getLocationStats(): Promise<any[]> {
107 return this.createQueryBuilder('tribute')
108 .select([
109 'tribute.location as location',
110 'COUNT(*) as tributeCount',
111 'SUM(tribute.value) as totalValue',
112 'AVG(tribute.value) as avgValue'
113 ])
114 .groupBy('tribute.location')
115 .having('COUNT(*) >= 3') // Only locations with 3+ tributes
116 .orderBy('totalValue', 'DESC')
117 .getRawMany();
118 }
119}
120
121// repositories/legionaries.repository.ts
122@EntityRepository(Legionary)
123export class LegionaryRepository extends Repository<Legionary> {
124 // Legionary ranking
125 async getRanking(criteria: {
126 orderBy: 'tributeCount' | 'totalValue' | 'experience';
127 includeInactive?: boolean;
128 }): Promise<Legionary[]> {
129 let query = this.createQueryBuilder('legionaries')
130 .leftJoinAndSelect('legionaries.tributes', 'tributes')
131 .leftJoinAndSelect('legionaries.centurion', 'centurion');
132
133 if (!criteria.includeInactive) {
134 query = query.where('legionaries.isActive = true');
135 }
136
137 switch (criteria.orderBy) {
138 case 'tributeCount':
139 query = query.orderBy('legionaries.tributeCount', 'DESC');
140 break;
141 case 'totalValue':
142 query = query.orderBy('legionaries.totalTributeValue', 'DESC');
143 break;
144 case 'experience':
145 query = query.orderBy('legionaries.skills->>\'tributeHunting\'', 'DESC');
146 break;
147 }
148
149 return query.getMany();
150 }
151
152 // Find nearby legionaries
153 async findNearby(
154 latitude: number,
155 longitude: number,
156 radiusKm: number = 100
157 ): Promise<Legionary[]> {
158 return this.createQueryBuilder('legionaries')
159 .where('legionaries.lastKnownLatitude IS NOT NULL')
160 .andWhere('legionaries.lastKnownLongitude IS NOT NULL')
161 .andWhere(`
162 (6371 * acos(
163 cos(radians(:lat)) * cos(radians(legionaries.lastKnownLatitude)) *
164 cos(radians(legionaries.lastKnownLongitude) - radians(:lng)) +
165 sin(radians(:lat)) * sin(radians(legionaries.lastKnownLatitude))
166 )) <= :radius
167 `)
168 .setParameters({ lat: latitude, lng: longitude, radius: radiusKm })
169 .orderBy(`
170 (6371 * acos(
171 cos(radians(${latitude})) * cos(radians(legionaries.lastKnownLatitude)) *
172 cos(radians(legionaries.lastKnownLongitude) - radians(${longitude})) +
173 sin(radians(${latitude})) * sin(radians(legionaries.lastKnownLatitude))
174 ))
175 `, 'ASC')
176 .getMany();
177 }
178
179 // Skills analysis
180 async getSkillsAnalysis(): Promise<any> {
181 const result = await this.createQueryBuilder('legionaries')
182 .select([
183 'AVG((skills->>\'navigation\')::numeric) as avgNavigation',
184 'AVG((skills->>\'combat\')::numeric) as avgCombat',
185 'AVG((skills->>\'negotiation\')::numeric) as avgNegotiation',
186 'AVG((skills->>\'leadership\')::numeric) as avgLeadership',
187 'AVG((skills->>\'tributeHunting\')::numeric) as avgTributeHunting'
188 ])
189 .where('legionaries.isActive = true')
190 .andWhere('legionaries.skills IS NOT NULL')
191 .getRawOne();
192
193 return {
194 navigation: parseFloat(result.avgnavigation) || 0,
195 combat: parseFloat(result.avgcombat) || 0,
196 negotiation: parseFloat(result.avgnegotiation) || 0,
197 leadership: parseFloat(result.avgleadership) || 0,
198 tributeHunting: parseFloat(result.avgtribute_hunting) || 0
199 };
200 }
201}
202
203// repositories/transaction.repository.ts
204@EntityRepository(Transaction)
205export class TransactionRepository extends Repository<Transaction> {
206 // Legionary transaction history
207 async getLegionaryTransactionHistory(
208 legionariesId: number,
209 limit: number = 50
210 ): Promise<Transaction[]> {
211 return this.createQueryBuilder('transaction')
212 .leftJoinAndSelect('transaction.fromLegionary', 'fromLegionary')
213 .leftJoinAndSelect('transaction.toLegionary', 'toLegionary')
214 .leftJoinAndSelect('transaction.tribute', 'tribute')
215 .leftJoinAndSelect('transaction.tradedTribute', 'tradedTribute')
216 .where('transaction.fromLegionary.id = :legionariesId OR transaction.toLegionary.id = :legionariesId',
217 { legionariesId })
218 .orderBy('transaction.createdAt', 'DESC')
219 .limit(limit)
220 .getMany();
221 }
222
223 // Transaction statistics
224 async getTransactionStats(period: 'day' | 'week' | 'month' = 'month'): Promise<any> {
225 const dateFormat = {
226 day: 'YYYY-MM-DD',
227 week: 'YYYY-"W"WW',
228 month: 'YYYY-MM'
229 };
230
231 return this.createQueryBuilder('transaction')
232 .select([
233 `TO_CHAR(transaction.createdAt, '${dateFormat[period]}') as period`,
234 'transaction.type as type',
235 'COUNT(*) as count',
236 'SUM(transaction.value) as totalValue',
237 'AVG(transaction.value) as avgValue'
238 ])
239 .where('transaction.status = :status', { status: 'COMPLETED' })
240 .groupBy('period, transaction.type')
241 .orderBy('period', 'DESC')
242 .getRawMany();
243 }
244
245 // Suspicious transactions
246 async findSuspiciousTransactions(): Promise<Transaction[]> {
247 return this.createQueryBuilder('transaction')
248 .leftJoinAndSelect('transaction.fromLegionary', 'fromLegionary')
249 .leftJoinAndSelect('transaction.toLegionary', 'toLegionary')
250 .leftJoinAndSelect('transaction.tribute', 'tribute')
251 .where(qb => {
252 const subQuery = qb.subQuery()
253 .select('AVG(t.value)')
254 .from(Transaction, 't')
255 .where('t.type = transaction.type')
256 .getQuery();
257 return `transaction.value > 10 * (${subQuery})`1 })
2 .orWhere('transaction.value > 100000') // Very high values
3 .orderBy('transaction.value', 'DESC')
4 .getMany();
5 }
6}1// services/tribute-management.service.ts
2@Injectable()
3export class TributeManagementService {
4 constructor(
5 @InjectRepository(TributeRepository)
6 private tributeRepository: TributeRepository,
7 @InjectRepository(LegionaryRepository)
8 private legionariesRepository: LegionaryRepository,
9 @InjectRepository(TransactionRepository)
10 private transactionRepository: TransactionRepository,
11 private dataSource: DataSource
12 ) {}
13
14 // Transfer tribute between legionaries
15 async transferTribute(
16 tributeId: number,
17 fromLegionaryId: number,
18 toLegionaryId: number,
19 notes?: string
20 ): Promise<Transaction> {
21 return await this.dataSource.transaction(async manager => {
22 // Validation
23 const tribute = await manager.findOne(Tribute, {
24 where: { id: tributeId, owner: { id: fromLegionaryId } },
25 relations: ['owner']
26 });
27
28 if (!tribute) {
29 throw new BadRequestException('Tribute not found or not owned by sender');
30 }
31
32 if (tribute.isCursed) {
33 throw new BadRequestException('Cannot transfer cursed tributes');
34 }
35
36 const recipient = await manager.findOne(Legionary, {
37 where: { id: toLegionaryId, isActive: true }
38 });
39
40 if (!recipient) {
41 throw new BadRequestException('Recipient not found or inactive');
42 }
43
44 // Check limits
45 await this.validateLegionaryCapacity(recipient, tribute.value);
46
47 // Create transaction
48 const transaction = manager.create(Transaction, {
49 type: 'TRANSFER',
50 status: 'PENDING',
51 value: tribute.value,
52 notes,
53 fromLegionary: tribute.owner,
54 toLegionary: recipient,
55 tribute
56 });
57
58 await manager.save(transaction);
59
60 // Execute transfer
61 tribute.owner = recipient;
62 await manager.save(tribute);
63
64 // Update counters
65 await manager.decrement(Legionary, { id: fromLegionaryId }, 'tributeCount', 1);
66 await manager.decrement(Legionary, { id: fromLegionaryId }, 'totalTributeValue', tribute.value);
67 await manager.increment(Legionary, { id: toLegionaryId }, 'tributeCount', 1);
68 await manager.increment(Legionary, { id: toLegionaryId }, 'totalTributeValue', tribute.value);
69
70 // Complete transaction
71 transaction.status = 'COMPLETED';
72 transaction.completedAt = new Date();
73 await manager.save(transaction);
74
75 return transaction;
76 });
77 }
78
79 // Tribute exchange
80 async tradeTributes(
81 legionaries1Id: number,
82 tribute1Id: number,
83 legionaries2Id: number,
84 tribute2Id: number
85 ): Promise<{ transaction1: Transaction; transaction2: Transaction }> {
86 return await this.dataSource.transaction(async manager => {
87 // Fetch tributes with owners
88 const [tribute1, tribute2] = await Promise.all([
89 manager.findOne(Tribute, {
90 where: { id: tribute1Id, owner: { id: legionaries1Id } },
91 relations: ['owner']
92 }),
93 manager.findOne(Tribute, {
94 where: { id: tribute2Id, owner: { id: legionaries2Id } },
95 relations: ['owner']
96 })
97 ]);
98
99 if (!tribute1 || !tribute2) {
100 throw new BadRequestException('One or both tributes not found');
101 }
102
103 if (tribute1.isCursed || tribute2.isCursed) {
104 throw new BadRequestException('Cannot trade cursed tributes');
105 }
106
107 // Check trade fairness (max 20% difference)
108 const valueDiff = Math.abs(tribute1.value - tribute2.value);
109 const maxValue = Math.max(tribute1.value, tribute2.value);
110 if (valueDiff > maxValue * 0.2) {
111 throw new BadRequestException('Trade values too different (max 20% difference allowed)');
112 }
113
114 // Create transactions
115 const transaction1 = manager.create(Transaction, {
116 type: 'TRADE',
117 status: 'PENDING',
118 value: tribute1.value,
119 fromLegionary: tribute1.owner,
120 toLegionary: tribute2.owner,
121 tribute: tribute1,
122 tradedTribute: tribute2
123 });
124
125 const transaction2 = manager.create(Transaction, {
126 type: 'TRADE',
127 status: 'PENDING',
128 value: tribute2.value,
129 fromLegionary: tribute2.owner,
130 toLegionary: tribute1.owner,
131 tribute: tribute2,
132 tradedTribute: tribute1
133 });
134
135 await manager.save([transaction1, transaction2]);
136
137 // Execute exchange
138 const originalOwner1 = tribute1.owner;
139 const originalOwner2 = tribute2.owner;
140
141 tribute1.owner = originalOwner2;
142 tribute2.owner = originalOwner1;
143
144 await manager.save([tribute1, tribute2]);
145
146 // Update total values (if different)
147 const valueChange = tribute1.value - tribute2.value;
148 if (valueChange !== 0) {
149 await manager.increment(Legionary, { id: legionaries1Id }, 'totalTributeValue', -valueChange);
150 await manager.increment(Legionary, { id: legionaries2Id }, 'totalTributeValue', valueChange);
151 }
152
153 // Complete transactions
154 transaction1.status = 'COMPLETED';
155 transaction1.completedAt = new Date();
156 transaction2.status = 'COMPLETED';
157 transaction2.completedAt = new Date();
158
159 await manager.save([transaction1, transaction2]);
160
161 return { transaction1, transaction2 };
162 });
163 }
164
165 // Expert tribute appraisal
166 async appraiseTribute(
167 tributeId: number,
168 appraiserName: string,
169 estimatedValue: number,
170 marketValue: number,
171 confidenceLevel: number,
172 notes: string
173 ): Promise<TributeAppraisal> {
174 const tribute = await this.tributeRepository.findOne({
175 where: { id: tributeId }
176 });
177
178 if (!tribute) {
179 throw new NotFoundException('Tribute not found');
180 }
181
182 const appraisal = this.dataSource.getRepository(TributeAppraisal).create({
183 tribute,
184 appraiserName,
185 estimatedValue,
186 marketValue,
187 confidenceLevel,
188 notes
189 });
190
191 return await this.dataSource.getRepository(TributeAppraisal).save(appraisal);
192 }
193
194 private async validateLegionaryCapacity(legionaries: Legionary, additionalValue: number): Promise<void> {
195 const rankLimits = {
196 'Tiro': { count: 5, value: 10000 },
197 'Tesserarius': { count: 10, value: 25000 },
198 'Speculator': { count: 15, value: 50000 },
199 'Optio': { count: 25, value: 100000 },
200 'Centurion': { count: 50, value: 500000 },
201 'Legate': { count: 100, value: 1000000 }
202 };
203
204 const limits = rankLimits[legionaries.rank] || rankLimits['Tiro'];
205
206 if (legionaries.tributeCount >= limits.count) {
207 throw new BadRequestException(
208 `Legionary of rank '${legionaries.rank}' cannot own more than ${limits.count} tributes`
209 );
210 }
211
212 if (legionaries.totalTributeValue + additionalValue > limits.value) {
213 throw new BadRequestException(
214 `Total tribute value would exceed limit for rank '${legionaries.rank}' (${limits.value} gold)`
215 );
216 }
217 }
218}
219
220// services/analytics.service.ts
221@Injectable()
222export class AnalyticsService {
223 constructor(
224 @InjectRepository(TributeRepository)
225 private tributeRepository: TributeRepository,
226 @InjectRepository(LegionaryRepository)
227 private legionariesRepository: LegionaryRepository,
228 @InjectRepository(TransactionRepository)
229 private transactionRepository: TransactionRepository
230 ) {}
231
232 // Dashboard with key metrics
233 async getDashboardMetrics(): Promise<any> {
234 const [
235 totalTributes,
236 totalLegionarys,
237 totalValue,
238 recentTransactions,
239 marketAnalysis,
240 topCollectors
241 ] = await Promise.all([
242 this.tributeRepository.count(),
243 this.legionariesRepository.count({ where: { isActive: true } }),
244 this.getTotalTributeValue(),
245 this.transactionRepository.count({
246 where: {
247 createdAt: MoreThan(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
248 }
249 }),
250 this.tributeRepository.getMarketAnalysis(),
251 this.tributeRepository.getTopCollectors(5)
252 ]);
253
254 return {
255 overview: {
256 totalTributes,
257 totalLegionarys,
258 totalValue,
259 recentTransactions
260 },
261 marketAnalysis,
262 topCollectors
263 };
264 }
265
266 private async getTotalTributeValue(): Promise<number> {
267 const result = await this.tributeRepository
268 .createQueryBuilder('tribute')
269 .select('SUM(tribute.value)', 'total')
270 .getRawOne();
271
272 return parseFloat(result.total) || 0;
273 }
274
275 // Forecasts and trends
276 async getTrends(): Promise<any> {
277 // Trend analysis implementation
278 const transactionStats = await this.transactionRepository.getTransactionStats('month');
279
280 return {
281 monthlyTransactions: transactionStats,
282 // Other analyses...
283 };
284 }
285}1// seeds/complete-legionaries-system.seeder.ts
2export default class CompleteLegionarySystemSeeder implements Seeder {
3 public async run(
4 dataSource: DataSource,
5 factoryManager: SeederFactoryManager
6 ): Promise<any> {
7 // 1. Basic data
8 await this.seedBasicData(dataSource);
9
10 // 2. Legionaries and centurions
11 await this.seedLegionarys(dataSource);
12
13 // 3. Tributes and maps
14 await this.seedTributesAndMaps(dataSource);
15
16 // 4. Test transactions
17 await this.seedTransactions(dataSource);
18
19 console.log(' Complete legionaries tribute system seeded successfully!');
20 }
21
22 private async seedBasicData(dataSource: DataSource): Promise<void> {
23 // Basic data seeding implementation
24 }
25
26 private async seedLegionarys(dataSource: DataSource): Promise<void> {
27 // Legionary seeding implementation
28 }
29
30 private async seedTributesAndMaps(dataSource: DataSource): Promise<void> {
31 // Tribute and maps seeding implementation
32 }
33
34 private async seedTransactions(dataSource: DataSource): Promise<void> {
35 // Transactions seeding implementation
36 }
37}1// controllers/tribute-management.controller.ts
2@Controller('tribute-management')
3@UseGuards(AuthGuard)
4export class TributeManagementController {
5 constructor(
6 private tributeManagementService: TributeManagementService,
7 private analyticsService: AnalyticsService
8 ) {}
9
10 @Get('dashboard')
11 async getDashboard() {
12 return await this.analyticsService.getDashboardMetrics();
13 }
14
15 @Post('transfer')
16 async transferTribute(@Body() transferDto: TransferTributeDto) {
17 return await this.tributeManagementService.transferTribute(
18 transferDto.tributeId,
19 transferDto.fromLegionaryId,
20 transferDto.toLegionaryId,
21 transferDto.notes
22 );
23 }
24
25 @Post('trade')
26 async tradeTributes(@Body() tradeDto: TradeTributesDto) {
27 return await this.tributeManagementService.tradeTributes(
28 tradeDto.legionaries1Id,
29 tradeDto.tribute1Id,
30 tradeDto.legionaries2Id,
31 tradeDto.tribute2Id
32 );
33 }
34
35 @Get('analytics/trends')
36 async getTrends() {
37 return await this.analyticsService.getTrends();
38 }
39
40 // More endpoints...
41}1// tests/tribute-management.integration.spec.ts
2describe('Tribute Management Integration', () => {
3 let app: INestApplication;
4 let dataSource: DataSource;
5
6 beforeAll(async () => {
7 const moduleFixture: TestingModule = await Test.createTestingModule({
8 imports: [AppModule],
9 }).compile();
10
11 app = moduleFixture.createNestApplication();
12 dataSource = moduleFixture.get<DataSource>(DataSource);
13 await app.init();
14 });
15
16 beforeEach(async () => {
17 // Clean database before each test
18 await dataSource.synchronize(true);
19 });
20
21 describe('Tribute transfer', () => {
22 it('should transfer tribute successfully', async () => {
23 // Setup test data
24 const legionaries1 = await createTestLegionary();
25 const legionaries2 = await createTestLegionary();
26 const tribute = await createTestTribute(legionaries1.id);
27
28 // Execute transfer
29 const response = await request(app.getHttpServer())
30 .post('/tribute-management/transfer')
31 .send({
32 tributeId: tribute.id,
33 fromLegionaryId: legionaries1.id,
34 toLegionaryId: legionaries2.id,
35 notes: 'Test transfer'
36 })
37 .expect(201);
38
39 // Verify results
40 expect(response.body.status).toBe('COMPLETED');
41
42 // Verify database state
43 const updatedTribute = await dataSource
44 .getRepository(Tribute)
45 .findOne({ where: { id: tribute.id }, relations: ['owner'] });
46
47 expect(updatedTribute.owner.id).toBe(legionaries2.id);
48 });
49 });
50
51 // More tests...
52});1src/
2├── entities/ # All entities with relations
3├── repositories/ # Custom repositories with logic
4├── services/ # Business logic services
5├── controllers/ # REST API endpoints
6├── dto/ # Data Transfer Objects
7├── migrations/ # Database migrations
8├── seeds/ # Data seeders
9├── tests/ # Integration tests
10└── validators/ # Custom validation classesCore Features:
Advanced Features:
Technical quality (40%):
Functionality (30%):
Code quality (20%):
Tests and reliability (10%):
Congratulations! This is your first real enterprise project with NestJS and TypeORM. Here you demonstrate:
Advanced database architecture with complete relations Enterprise-level business logic with transactions and validation Sophisticated querying with analytics and reporting Robust error handling and security measures Real-time dashboards and data visualization
This project is a real tribute in your portfolio - it shows you can build scalable, enterprise-grade applications with complex business logic!
Consul Caesar.js is proud of your progress!