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
130 changes: 130 additions & 0 deletions README_REVENUE_SHARING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Revenue Sharing Module

## Overview
The Revenue Sharing Module enables smart contract-like revenue splitting between stakeholders in the Veritix platform. This module allows event organizers to define how revenue from ticket sales is distributed among different parties.

## Features
1. **Flexible Revenue Splitting**: Define percentage-based or fixed-amount revenue splits
2. **Automatic Distribution**: Revenue is automatically distributed after ticket sales
3. **Dashboard Integration**: View revenue breakdowns in the dashboard
4. **Stakeholder Management**: Associate multiple stakeholders with revenue shares

## Module Structure
```
src/modules/revenue-sharing/
├── revenue-sharing.entity.ts # RevenueShareRule entity definition
├── revenue-sharing.service.ts # Business logic for revenue distribution
├── revenue-sharing.controller.ts # API endpoints for revenue management
├── revenue-sharing.module.ts # Module definition
├── dto/
│ └── create-revenue-split.dto.ts # DTO for defining revenue splits
└── revenue-sharing.service.spec.ts # Unit tests
```

## Entity: RevenueShareRule
The [RevenueShareRule](file:///c:/Users/k-aliyu/Documents/GitHub/veritix-backend/src/modules/revenue-sharing/revenue-sharing.entity.ts#L16-L38) entity defines how revenue should be split for an event:

- `event`: The event associated with this revenue split
- `stakeholder`: The user who will receive a portion of the revenue
- `shareType`: Either PERCENTAGE or FIXED_AMOUNT
- `shareValue`: The percentage (0-100) or fixed amount to distribute
- `isActive`: Whether this rule is currently active

## API Endpoints

### Define Revenue Split
```
POST /revenue-sharing/events/:eventId/splits
```
Define how revenue should be split for an event.

**Request Body:**
```json
{
"splits": [
{
"stakeholderId": "user1",
"shareType": "percentage",
"shareValue": 70
},
{
"stakeholderId": "user2",
"shareType": "percentage",
"shareValue": 30
}
]
}
```

### Distribute Revenue
```
POST /revenue-sharing/events/:eventId/distribute
```
Trigger revenue distribution for an event based on defined rules.

**Request Body:**
```json
{
"totalRevenue": 10000
}
```

### Get Revenue Breakdown
```
GET /revenue-sharing/events/:eventId/breakdown
```
Retrieve revenue breakdown for dashboard display.

## Integration with Ticket Sales
The revenue sharing module integrates with the ticket sales process:

1. When tickets are sold, the total revenue is calculated
2. The system automatically calls the distribute revenue function
3. Revenue is split according to the defined rules
4. Each stakeholder receives their portion

## Example Usage

### 1. Define a 70/30 Revenue Split
An event organizer wants to split revenue 70% to themselves and 30% to a venue partner:

```typescript
const splits = [
{
stakeholderId: "organizer-user-id",
shareType: RevenueShareType.PERCENTAGE,
shareValue: 70
},
{
stakeholderId: "venue-partner-id",
shareType: RevenueShareType.PERCENTAGE,
shareValue: 30
}
];

await revenueSharingService.defineRevenueSplit("event-id", splits);
```

### 2. Distribute Revenue After Sales
After an event generates $10,000 in ticket sales:

```typescript
const breakdown = await revenueSharingService.distributeRevenue("event-id", 10000);
// Organizer receives $7,000
// Venue partner receives $3,000
```

## Future Enhancements
1. **Smart Contract Integration**: Integrate with blockchain smart contracts for automated execution
2. **Escrow Services**: Hold funds in escrow until event completion
3. **Dispute Resolution**: Handle revenue disputes between stakeholders
4. **Audit Trail**: Detailed logging of all revenue distribution activities
5. **Multiple Distribution Models**: Support for more complex distribution scenarios

## Testing
The module includes comprehensive unit tests to ensure correct revenue calculation and distribution logic.

To run tests:
```bash
npm run test revenue-sharing
```
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import { EventModule } from './modules/event/event.module';
HealthModule,
UsersModule,
TicketsModule,
RevenueSharingModule,
EventModule,

],
providers: [AppService, OrganizerService],
controllers: [AppController, OrganizerController],
Expand Down
7 changes: 7 additions & 0 deletions src/modules/event/event.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm';
import { User } from '../../user/user.entity';
import { RevenueShareRule } from '../../modules/revenue-sharing/revenue-sharing.entity';
import { Ticket } from '../../ticket/ticket.entity';

@Entity()
Expand Down Expand Up @@ -43,6 +44,12 @@ export class Event {
@ManyToOne(() => User, user => user.id, { eager: true })
organizer: User;


@OneToMany(() => RevenueShareRule, rule => rule.event)
revenueShareRules: RevenueShareRule[];
}

@OneToMany(() => Ticket, (ticket) => ticket.event)
tickets: Ticket[];
}

9 changes: 9 additions & 0 deletions src/modules/revenue-sharing/dto/create-revenue-split.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { RevenueShareType } from '../revenue-sharing.entity';

export class CreateRevenueSplitDto {
splits: {
stakeholderId: string;
shareType: RevenueShareType;
shareValue: number
}[];
}
55 changes: 55 additions & 0 deletions src/modules/revenue-sharing/revenue-sharing.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Controller, Get, Post, Param, Body, Logger } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
import { RevenueSharingService, RevenueBreakdown } from './revenue-sharing.service';
import { RevenueShareRule, RevenueShareType } from './revenue-sharing.entity';

class DefineRevenueSplitDto {
splits: {
stakeholderId: string;
shareType: RevenueShareType;
shareValue: number
}[];
}

@ApiTags('Revenue Sharing')
@Controller('revenue-sharing')
export class RevenueSharingController {
private readonly logger = new Logger(RevenueSharingController.name);

constructor(private readonly revenueSharingService: RevenueSharingService) {}

@Post('events/:eventId/splits')
@ApiOperation({ summary: 'Define revenue split rules for an event' })
@ApiParam({ name: 'eventId', description: 'Event ID' })
@ApiBody({ type: DefineRevenueSplitDto })
@ApiResponse({ status: 201, description: 'Revenue split rules defined successfully' })
async defineRevenueSplit(
@Param('eventId') eventId: string,
@Body() dto: DefineRevenueSplitDto,
): Promise<RevenueShareRule[]> {
this.logger.log(`Defining revenue split for event ${eventId}`);
return this.revenueSharingService.defineRevenueSplit(eventId, dto.splits);
}

@Post('events/:eventId/distribute')
@ApiOperation({ summary: 'Distribute revenue automatically after ticket sales' })
@ApiParam({ name: 'eventId', description: 'Event ID' })
@ApiResponse({ status: 200, description: 'Revenue distributed successfully' })
async distributeRevenue(
@Param('eventId') eventId: string,
@Body('totalRevenue') totalRevenue: number,
): Promise<RevenueBreakdown> {
this.logger.log(`Distributing revenue for event ${eventId}`);
return this.revenueSharingService.distributeRevenue(eventId, totalRevenue);
}

@Get('events/:eventId/breakdown')
@ApiOperation({ summary: 'Get revenue breakdown for dashboard' })
@ApiParam({ name: 'eventId', description: 'Event ID' })
@ApiResponse({ status: 200, description: 'Revenue breakdown retrieved successfully' })
async getRevenueBreakdown(
@Param('eventId') eventId: string,
): Promise<RevenueBreakdown> {
return this.revenueSharingService.getRevenueBreakdown(eventId);
}
}
39 changes: 39 additions & 0 deletions src/modules/revenue-sharing/revenue-sharing.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Event } from '../event/event.entity';
import { User } from '../../user/user.entity';

export enum RevenueShareType {
PERCENTAGE = 'percentage',
FIXED_AMOUNT = 'fixed_amount',
}

@Entity()
export class RevenueShareRule {
@PrimaryGeneratedColumn('uuid')
id: string;

@ManyToOne(() => Event, event => event.id, { eager: true })
event: Event;

@ManyToOne(() => User, user => user.id, { eager: true })
stakeholder: User;

@Column({
type: 'enum',
enum: RevenueShareType,
default: RevenueShareType.PERCENTAGE,
})
shareType: RevenueShareType;

@Column({ type: 'decimal', precision: 5, scale: 2 })
shareValue: number; // Percentage (0-100) or fixed amount

@Column({ default: true })
isActive: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
13 changes: 13 additions & 0 deletions src/modules/revenue-sharing/revenue-sharing.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RevenueShareRule } from './revenue-sharing.entity';
import { RevenueSharingService } from './revenue-sharing.service';
import { RevenueSharingController } from './revenue-sharing.controller';

@Module({
imports: [TypeOrmModule.forFeature([RevenueShareRule])],
controllers: [RevenueSharingController],
providers: [RevenueSharingService],
exports: [RevenueSharingService],
})
export class RevenueSharingModule {}
101 changes: 101 additions & 0 deletions src/modules/revenue-sharing/revenue-sharing.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RevenueSharingService } from './revenue-sharing.service';
import { RevenueShareRule, RevenueShareType } from './revenue-sharing.entity';

describe('RevenueSharingService', () => {
let service: RevenueSharingService;
let repository: Repository<RevenueShareRule>;

const mockRevenueShareRuleRepository = {
find: jest.fn(),
save: jest.fn(),
update: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RevenueSharingService,
{
provide: getRepositoryToken(RevenueShareRule),
useValue: mockRevenueShareRuleRepository,
},
],
}).compile();

service = module.get<RevenueSharingService>(RevenueSharingService);
repository = module.get<Repository<RevenueShareRule>>(
getRepositoryToken(RevenueShareRule),
);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('defineRevenueSplit', () => {
it('should define revenue split rules', async () => {
const eventId = 'event1';
const splits = [
{
stakeholderId: 'user1',
shareType: RevenueShareType.PERCENTAGE,
shareValue: 70,
},
{
stakeholderId: 'user2',
shareType: RevenueShareType.PERCENTAGE,
shareValue: 30,
},
];

mockRevenueShareRuleRepository.update.mockResolvedValue(undefined);
mockRevenueShareRuleRepository.save.mockResolvedValue(splits);

const result = await service.defineRevenueSplit(eventId, splits);

expect(mockRevenueShareRuleRepository.update).toHaveBeenCalledWith(
{ event: { id: eventId } },
{ isActive: false },
);
expect(result).toEqual(splits);
});
});

describe('calculateRevenueDistribution', () => {
it('should calculate revenue distribution for percentage splits', async () => {
const eventId = 'event1';
const totalRevenue = 1000;

const mockRules = [
{
id: 'rule1',
event: { id: eventId },
stakeholder: { id: 'user1', email: 'user1@example.com' },
shareType: RevenueShareType.PERCENTAGE,
shareValue: 70,
isActive: true,
},
{
id: 'rule2',
event: { id: eventId },
stakeholder: { id: 'user2', email: 'user2@example.com' },
shareType: RevenueShareType.PERCENTAGE,
shareValue: 30,
isActive: true,
},
];

mockRevenueShareRuleRepository.find.mockResolvedValue(mockRules);

const result = await service.calculateRevenueDistribution(eventId, totalRevenue);

expect(result.totalRevenue).toBe(totalRevenue);
expect(result.distributions).toHaveLength(2);
expect(result.distributions[0].amount).toBe(700);
expect(result.distributions[1].amount).toBe(300);
});
});
});
Loading
Loading