📝 Sort docs
This commit is contained in:
345
docs/GIFT_SUBSCRIPTIONS.md
Normal file
345
docs/GIFT_SUBSCRIPTIONS.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# Gift Subscriptions API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Gift Subscriptions feature allows users to purchase subscription gifts that can be redeemed by other users, enabling social gifting and subscription sharing within the DysonNetwork platform.
|
||||
|
||||
If you use it through the gateway, the `/api` should be replaced with the `/id`
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Purchase Gifts**: Users can buy subscriptions as gifts for specific recipients or as open gifts
|
||||
- **Gift Codes**: Each gift has a unique redemption code
|
||||
- **Flexible Redemption**: Open gifts can be redeemed by anyone, while targeted gifts are recipient-specific
|
||||
- **Security**: Prevents duplicate subscriptions and enforces account level requirements
|
||||
- **Integration**: Full integration with existing subscription, coupon, and pricing systems
|
||||
- **Clean User Experience**: Unpaid gifts are hidden from users and automatically cleaned up
|
||||
- **Automatic Maintenance**: Old unpaid gifts are removed after 24 hours
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All endpoints are authenticated and require a valid user session. The base path for gift endpoints is `/api/gifts`.
|
||||
|
||||
### 1. List Sent Gifts
|
||||
|
||||
Retrieve gifts you have purchased.
|
||||
|
||||
```http
|
||||
GET /api/gifts/sent?offset=0&take=20
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Response**: Array of `SnWalletGift` objects
|
||||
|
||||
### 2. List Received Gifts
|
||||
|
||||
Retrieve gifts sent to you or redeemed by you (for open gifts).
|
||||
|
||||
```http
|
||||
GET /api/gifts/received?offset=0&take=20
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Response**: Array of `SnWalletGift` objects
|
||||
|
||||
### 3. Get Specific Gift
|
||||
|
||||
Retrieve details for a specific gift.
|
||||
|
||||
```http
|
||||
GET /api/gifts/{giftId}
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `giftId`: GUID of the gift
|
||||
|
||||
**Response**: `SnWalletGift` object
|
||||
|
||||
### 4. Check Gift Code
|
||||
|
||||
Validate if a gift code can be redeemed by the current user.
|
||||
|
||||
```http
|
||||
GET /api/gifts/check/{giftCode}
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"gift_code": "ABCD1234EFGH",
|
||||
"subscription_identifier": "basic",
|
||||
"can_redeem": true,
|
||||
"error": null,
|
||||
"message": "Happy birthday!"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Purchase a Gift
|
||||
|
||||
Create and purchase a gift subscription.
|
||||
|
||||
```http
|
||||
POST /api/gifts/purchase
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"subscription_identifier": "premium",
|
||||
"recipient_id": "550e8400-e29b-41d4-a716-446655440000", // Optional: null for open gifts
|
||||
"payment_method": "in_app_wallet",
|
||||
"payment_details": {
|
||||
"currency": "irl"
|
||||
},
|
||||
"message": "Enjoy your premium subscription!", // Optional
|
||||
"coupon": "SAVE20", // Optional
|
||||
"gift_duration_days": 30, // Optional: defaults to 30
|
||||
"subscription_duration_days": 30 // Optional: defaults to 30
|
||||
}
|
||||
```
|
||||
|
||||
**Response**: `SnWalletGift` object
|
||||
|
||||
### 6. Redeem a Gift
|
||||
|
||||
Redeem a gift code to create a subscription for yourself.
|
||||
|
||||
```http
|
||||
POST /api/gifts/redeem
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"gift_code": "ABCD1234EFGH"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"gift": { ... },
|
||||
"subscription": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Mark Gift as Sent
|
||||
|
||||
Mark a gift as sent (ready for redemption).
|
||||
|
||||
```http
|
||||
POST /api/gifts/{giftId}/send
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `giftId`: GUID of the gift to mark as sent
|
||||
|
||||
### 8. Cancel a Gift
|
||||
|
||||
Cancel a gift before it has been redeemed.
|
||||
|
||||
```http
|
||||
POST /api/gifts/{giftId}/cancel
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `giftId`: GUID of the gift to cancel
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Client Implementation
|
||||
|
||||
Here are examples showing how to integrate gift subscriptions into your client application.
|
||||
|
||||
#### Example 1: Purchase a Gift for a Specific User
|
||||
|
||||
```javascript
|
||||
async function purchaseGiftForFriend(subscriptionId, friendId, message) {
|
||||
const response = await fetch('/api/gifts/purchase', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription_identifier: subscriptionId,
|
||||
recipient_id: friendId,
|
||||
payment_method: 'in_app_wallet',
|
||||
payment_details: { currency: 'irl' },
|
||||
message: message
|
||||
})
|
||||
});
|
||||
|
||||
const gift = await response.json();
|
||||
return gift.gift_code; // Share this code with the friend
|
||||
}
|
||||
```
|
||||
|
||||
#### Example 2: Create an Open Gift
|
||||
|
||||
```javascript
|
||||
async function createOpenGift(subscriptionId) {
|
||||
const response = await fetch('/api/gifts/purchase', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subscription_identifier: subscriptionId,
|
||||
payment_method: 'in_app_wallet',
|
||||
payment_details: { currency: 'irl' },
|
||||
message: 'Redeem this anywhere!'
|
||||
// No recipient_id makes it an open gift
|
||||
})
|
||||
});
|
||||
|
||||
const gift = await response.json();
|
||||
// Mark as sent to make it redeemable
|
||||
await markGiftAsSent(gift.id);
|
||||
return gift;
|
||||
}
|
||||
```
|
||||
|
||||
#### Example 3: Redeem a Gift Code
|
||||
|
||||
```javascript
|
||||
async function redeemGiftCode(giftCode) {
|
||||
// First, check if the gift can be redeemed
|
||||
const checkResponse = await fetch(`/api/gifts/check/${giftCode}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const checkResult = await checkResponse.json();
|
||||
|
||||
if (!checkResult.canRedeem) {
|
||||
throw new Error(checkResult.error);
|
||||
}
|
||||
|
||||
// If valid, redeem it
|
||||
const redeemResponse = await fetch('/api/gifts/redeem', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
gift_code: giftCode
|
||||
})
|
||||
});
|
||||
|
||||
const result = await redeemResponse.json();
|
||||
return result.subscription; // The newly created subscription
|
||||
}
|
||||
```
|
||||
|
||||
#### Example 4: Display User's Gift History
|
||||
|
||||
```javascript
|
||||
async function getGiftHistory() {
|
||||
// Get gifts I sent
|
||||
const sentResponse = await fetch('/api/gifts/sent', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const sentGifts = await sentResponse.json();
|
||||
|
||||
// Get gifts I received
|
||||
const receivedResponse = await fetch('/api/gifts/received', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const receivedGifts = await receivedResponse.json();
|
||||
|
||||
return { sent: sentGifts, received: receivedGifts };
|
||||
}
|
||||
```
|
||||
|
||||
## Gift Status Lifecycle
|
||||
|
||||
Gifts follow this status lifecycle:
|
||||
|
||||
1. **Created**: Initially purchased, can be cancelled or marked as sent
|
||||
- **Note**: Gifts in "Created" status are not visible to users and are automatically cleaned up after 24 hours if unpaid
|
||||
2. **Sent**: Made available for redemption, can be cancelled
|
||||
3. **Redeemed**: Successfully redeemed, creates a subscription
|
||||
4. **Cancelled**: Permanently cancelled, refund may be processed
|
||||
5. **Expired**: Expired without redemption
|
||||
|
||||
## Automatic Maintenance
|
||||
|
||||
The system includes automatic cleanup to maintain data integrity:
|
||||
|
||||
- **Unpaid Gift Cleanup**: Gifts that remain in "Created" status (unpaid) for more than 24 hours are automatically removed from the database
|
||||
- **User Visibility**: Only gifts that have been successfully paid and sent are visible in user gift lists
|
||||
- **Background Processing**: Cleanup runs hourly via scheduled jobs
|
||||
|
||||
This ensures a clean user experience while preventing accumulation of abandoned gift purchases.
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Purchase Validation
|
||||
- Subscription must exist and be valid
|
||||
- If coupon provided, it must be valid and applicable
|
||||
- Recipient account must exist (if specified)
|
||||
- User must meet level requirements for the subscription
|
||||
|
||||
### Redemption Validation
|
||||
- Gift code must exist
|
||||
- Gift must be in "Sent" status
|
||||
- Gift must not be expired
|
||||
- User must meet level requirements
|
||||
- User must not already have an active subscription of the same type
|
||||
- For targeted gifts, user must be the specified recipient
|
||||
|
||||
## Pricing & Payments
|
||||
|
||||
Gifts use the same pricing system as regular subscriptions:
|
||||
|
||||
- Base price from subscription template
|
||||
- Coupon discounts applied
|
||||
- Currency conversion as needed
|
||||
- Payment processing through existing payment methods
|
||||
|
||||
## Notification Events
|
||||
|
||||
The system sends push notifications for:
|
||||
|
||||
- **gifts.redeemed**: When someone redeems your gift
|
||||
- **gifts.claimed**: When the recipient redeems your targeted gift
|
||||
|
||||
Notifications include gift and subscription details for rich UI updates.
|
||||
|
||||
## Error Handling
|
||||
|
||||
Common error responses:
|
||||
|
||||
- `400 Bad Request`: Invalid parameters, validation failures
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
- `403 Forbidden`: Insufficient permissions
|
||||
- `404 Not Found`: Gift or subscription not found
|
||||
- `409 Conflict`: Business logic violations (duplicate subscriptions, etc.)
|
||||
|
||||
## Integration Notes
|
||||
|
||||
### Database Schema
|
||||
The feature adds a `wallet_gifts` table with relationships to:
|
||||
- `accounts` (gifter, recipient, redeemer)
|
||||
- `wallet_subscriptions` (created subscription)
|
||||
- `wallet_coupons` (applied discounts)
|
||||
|
||||
### Backwards Compatibility
|
||||
- No changes to existing subscription endpoints
|
||||
- New gift-related endpoints are additive
|
||||
- Existing payment flows remain unchanged
|
||||
|
||||
### Performance Considerations
|
||||
- Gift codes are indexed for fast lookups
|
||||
- Status filters optimize database queries
|
||||
- Caching integrated with existing subscription caching
|
||||
|
||||
## Support
|
||||
|
||||
For implementation questions or issues, refer to the DysonNetwork API documentation or contact the development team.
|
||||
403
docs/LOTTERY.md
Normal file
403
docs/LOTTERY.md
Normal file
@@ -0,0 +1,403 @@
|
||||
:bug# Lottery System API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The DysonNetwork Lottery System provides a daily lottery where users can purchase tickets with custom number selections. Each day features a new draw with random winning numbers. Users purchase tickets using ISP (Dyson Network Points), with results announced each morning.
|
||||
|
||||
The API is handled by the DysonNetwork.Pass service. Which means if you use it with the Gateway the `/api` should be replaced with `/pass`
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Daily Draws**: Automated draws at midnight UTC
|
||||
- **Custom Number Selection**: Users choose 5 unique numbers (0-99) + 1 special number (0-99)
|
||||
- **Flexible Pricing**: Base cost 10 ISP + extra ISP per multiplier (e.g., multiplier=2 costs 20 ISP)
|
||||
- **Daily Limits**: One ticket purchase per user per day
|
||||
- **Prize System**: Multiple prize tiers based on matches
|
||||
- **Instant Payment**: Tickets purchased using in-app points
|
||||
- **Historical Records**: Complete draw history and statistics
|
||||
|
||||
## Data Models
|
||||
|
||||
### LotteryDrawStatus Enum
|
||||
```csharp
|
||||
public enum LotteryDrawStatus
|
||||
{
|
||||
Pending = 0, // Ticket awaiting draw
|
||||
Drawn = 1 // Ticket has been processed in draw
|
||||
}
|
||||
```
|
||||
|
||||
### SnLottery Model
|
||||
```csharp
|
||||
public class SnLottery : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public SnAccount Account { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
public List<int> RegionOneNumbers { get; set; } = new(); // 5 numbers (0-99)
|
||||
public int RegionTwoNumber { get; set; } // Special number (0-99)
|
||||
public int Multiplier { get; set; } = 1; // Prize multiplier (≥1)
|
||||
public LotteryDrawStatus DrawStatus { get; set; }
|
||||
public DateTime? DrawDate { get; set; } // Date when drawn
|
||||
}
|
||||
```
|
||||
|
||||
### SnLotteryRecord Model
|
||||
```csharp
|
||||
public class SnLotteryRecord : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTime DrawDate { get; set; }
|
||||
public List<int> WinningRegionOneNumbers { get; set; } = new(); // 5 winning numbers
|
||||
public int WinningRegionTwoNumber { get; set; } // Winning special number
|
||||
public int TotalTickets { get; set; } // Total tickets processed
|
||||
public int TotalPrizesAwarded { get; set; } // Number of winning tickets
|
||||
public long TotalPrizeAmount { get; set; } // Total ISP prize amount
|
||||
}
|
||||
```
|
||||
|
||||
## Prize Structure
|
||||
|
||||
| Region 1 Matches | Base Prize (ISP) | Notes |
|
||||
|-----------------|------------------|-------|
|
||||
| 0 | 0 | No prize |
|
||||
| 1 | 10 | Minimum win |
|
||||
| 2 | 20 | Double minimum |
|
||||
| 3 | 50 | Five times minimum |
|
||||
| 4 | 100 | Ten times minimum |
|
||||
| 5 | 1000 | Maximum prize |
|
||||
|
||||
**Special Number Bonus**: If Region 2 number matches, multiply any prize by 10x.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All endpoints require authentication via Bearer token.
|
||||
|
||||
### Purchase Ticket
|
||||
**POST** `/api/lotteries`
|
||||
|
||||
Creates a lottery order and deducts ISP from user's wallet.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"RegionOneNumbers": [5, 23, 47, 68, 89],
|
||||
"RegionTwoNumber": 42,
|
||||
"Multiplier": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "guid",
|
||||
"accountId": "guid",
|
||||
"createdAt": "2025-10-24T00:00:00Z",
|
||||
"status": "Paid",
|
||||
"currency": "isp",
|
||||
"amount": 10,
|
||||
"productIdentifier": "lottery"
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
- `RegionOneNumbers`: Exactly 5 unique integers between 0-99
|
||||
- `RegionTwoNumber`: Single integer between 0-99
|
||||
- `Multiplier`: Integer ≥ 1
|
||||
- User can only purchase 1 ticket per day
|
||||
|
||||
**Pricing:**
|
||||
- Base cost: 10 ISP
|
||||
- Additional cost: (Multiplier - 1) × 10 ISP
|
||||
- Total cost = (Multiplier × 10) ISP
|
||||
|
||||
### Get User Tickets
|
||||
**GET** `/api/lotteries`
|
||||
|
||||
Retrieves user's lottery tickets with pagination.
|
||||
|
||||
**Query Parameters:**
|
||||
- `offset` (optional, default 0): Page offset
|
||||
- `limit` (optional, default 20, max 100): Items per page
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "guid",
|
||||
"regionOneNumbers": [5, 23, 47, 68, 89],
|
||||
"regionTwoNumber": 42,
|
||||
"multiplier": 1,
|
||||
"drawStatus": "Pending",
|
||||
"drawDate": null,
|
||||
"createdAt": "2025-10-24T10:30:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Response Headers:**
|
||||
```
|
||||
X-Total: 42 // Total number of user's tickets
|
||||
```
|
||||
|
||||
### Get Specific Ticket
|
||||
**GET** `/api/lotteries/{id}`
|
||||
|
||||
Retrieves a specific lottery ticket by ID.
|
||||
|
||||
**Response:**
|
||||
Same structure as individual items from Get User Tickets.
|
||||
|
||||
**Error Responses:**
|
||||
- `404 Not Found`: Ticket doesn't exist or user doesn't own it
|
||||
|
||||
### Get Lottery Records
|
||||
**GET** `/api/lotteries/records`
|
||||
|
||||
Retrieves historical lottery draw results.
|
||||
|
||||
**Query Parameters:**
|
||||
- `startDate` (optional): Filter by draw date (YYYY-MM-DD)
|
||||
- `endDate` (optional): Filter by draw date (YYYY-MM-DD)
|
||||
- `offset` (optional, default 0): Page offset
|
||||
- `limit` (optional, default 20): Items per page
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "guid",
|
||||
"drawDate": "2025-10-24T00:00:00Z",
|
||||
"winningRegionOneNumbers": [7, 15, 23, 46, 82],
|
||||
"winningRegionTwoNumber": 19,
|
||||
"totalTickets": 245,
|
||||
"totalPrizesAwarded": 23,
|
||||
"totalPrizeAmount": 4820
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Frontend Integration (JavaScript/React)
|
||||
|
||||
```javascript
|
||||
// Purchase a lottery ticket
|
||||
async function purchaseLottery(numbers, specialNumber, multiplier = 1) {
|
||||
try {
|
||||
const response = await fetch('/api/lotteries', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${userToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
RegionOneNumbers: numbers, // Array of 5 unique numbers 0-99
|
||||
RegionTwoNumber: specialNumber, // Number 0-99
|
||||
Multiplier: multiplier // Optional, defaults to 1
|
||||
})
|
||||
});
|
||||
|
||||
const order = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Ticket purchased successfully!', order);
|
||||
// Refresh user ISP balance
|
||||
updateWalletBalance();
|
||||
} else {
|
||||
console.error('Purchase failed:', order);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get user's tickets
|
||||
async function getUserTickets() {
|
||||
try {
|
||||
const response = await fetch('/api/lotteries?limit=20', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userToken}`
|
||||
}
|
||||
});
|
||||
|
||||
const tickets = await response.json();
|
||||
const totalTickets = response.headers.get('X-Total');
|
||||
|
||||
return { tickets, total: parseInt(totalTickets) };
|
||||
} catch (error) {
|
||||
console.error('Error fetching tickets:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Get draw history
|
||||
async function getDrawHistory() {
|
||||
try {
|
||||
const response = await fetch('/api/lotteries/records', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userToken}`
|
||||
}
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching history:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mobile Integration (React Native/TypeScript)
|
||||
|
||||
```typescript
|
||||
interface LotteryTicket {
|
||||
id: string;
|
||||
regionOneNumbers: number[];
|
||||
regionTwoNumber: number;
|
||||
multiplier: number;
|
||||
drawStatus: 'Pending' | 'Drawn';
|
||||
drawDate?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface PurchaseRequest {
|
||||
RegionOneNumbers: number[];
|
||||
RegionTwoNumber: number;
|
||||
Multiplier: number;
|
||||
}
|
||||
|
||||
class LotteryService {
|
||||
private apiUrl = 'https://your-api-domain.com/api/lotteries';
|
||||
|
||||
async purchaseTicket(
|
||||
ticket: Omit<PurchaseRequest, 'RegionOneNumbers'> & { numbers: number[] },
|
||||
token: string
|
||||
): Promise<any> {
|
||||
const request: PurchaseRequest = {
|
||||
RegionOneNumbers: ticket.numbers,
|
||||
RegionTwoNumber: ticket.RegionTwoNumber,
|
||||
Multiplier: ticket.Multiplier
|
||||
};
|
||||
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getTickets(token: string, offset = 0, limit = 20): Promise<LotteryTicket[]> {
|
||||
const response = await fetch(`${this.apiUrl}?offset=${offset}&limit=${limit}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getDrawRecords(token: string): Promise<any[]> {
|
||||
const response = await fetch(`${this.apiUrl}/records`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Number Validation
|
||||
|
||||
```javascript
|
||||
function validateLotteryNumbers(numbers, specialNumber, multiplier = 1) {
|
||||
// Validate region one numbers
|
||||
if (!Array.isArray(numbers) || numbers.length !== 5) {
|
||||
return { valid: false, error: 'Must select exactly 5 numbers' };
|
||||
}
|
||||
|
||||
const uniqueNumbers = new Set(numbers);
|
||||
if (uniqueNumbers.size !== 5) {
|
||||
return { valid: false, error: 'Numbers must be unique' };
|
||||
}
|
||||
|
||||
// Check range 0-99
|
||||
for (const num of numbers) {
|
||||
if (!Number.isInteger(num) || num < 0 || num > 99) {
|
||||
return { valid: false, error: 'Numbers must be integers between 0-99' };
|
||||
}
|
||||
}
|
||||
|
||||
// Validate special number
|
||||
if (!Number.isInteger(specialNumber) || specialNumber < 0 || specialNumber > 99) {
|
||||
return { valid: false, error: 'Special number must be between 0-99' };
|
||||
}
|
||||
|
||||
// Validate multiplier
|
||||
if (!Number.isInteger(multiplier) || multiplier < 1) {
|
||||
return { valid: false, error: 'Multiplier must be 1 or greater' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// Example usage
|
||||
const validation = validateLotteryNumbers([5, 12, 23, 47, 89], 42, 2);
|
||||
if (!validation.valid) {
|
||||
console.error(validation.error);
|
||||
}
|
||||
```
|
||||
|
||||
## Daily Draw Schedule
|
||||
|
||||
- **Draw Time**: Every midnight UTC (00:00 UTC)
|
||||
- **Processing**: Only tickets from the previous day are included
|
||||
- **Prize Distribution**: Winners automatically receive ISP credits
|
||||
- **History**: Draws are preserved indefinitely
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Error Codes
|
||||
- `400 Bad Request`: Invalid request data (bad numbers, duplicate purchase, etc.)
|
||||
- `401 Unauthorized`: Missing or invalid authentication token
|
||||
- `404 Not Found`: Ticket doesn't exist or access denied
|
||||
- `403 Forbidden`: Insufficient permissions (admin endpoints)
|
||||
|
||||
### Error Response Format
|
||||
```json
|
||||
{
|
||||
"message": "You can only purchase one lottery per day.",
|
||||
"type": "ArgumentException",
|
||||
"statusCode": 400
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Test Cases
|
||||
1. **Valid Purchase**: Select valid numbers, verify wallet deduction
|
||||
2. **Invalid Numbers**: Try duplicate region one numbers, out-of-range values
|
||||
3. **Daily Limit**: Attempt second purchase in same day
|
||||
4. **Insufficient Funds**: Try purchase without enough ISP
|
||||
5. **Draw Processing**: Verify winning tickets receive correct prizes
|
||||
6. **Historical Data**: Check draw records match processed tickets
|
||||
|
||||
### Test Data Examples
|
||||
```javascript
|
||||
// Valid ticket
|
||||
{ numbers: [1, 15, 23, 67, 89], special: 42, multiplier: 1 }
|
||||
|
||||
// Invalid - duplicate numbers
|
||||
{ numbers: [1, 15, 23, 15, 89], special: 42, multiplier: 1 }
|
||||
|
||||
// Invalid - out of range
|
||||
{ numbers: [1, 15, 23, 67, 150], special: 42, multiplier: 1 }
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For API integration questions or support:
|
||||
- Check network documentation for authentication details
|
||||
- Contact Dyson Network development team for assistance
|
||||
- Monitor API response headers for pagination metadata
|
||||
63
docs/ONBOARDING_FLOW.md
Normal file
63
docs/ONBOARDING_FLOW.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Client-Side Onboarding Flow for New Users
|
||||
|
||||
This document outlines the steps for a client application to handle the onboarding of new users who authenticate via a third-party provider.
|
||||
|
||||
## 1. Initiate the OIDC Login Flow
|
||||
|
||||
This step remains the same as a standard OIDC authorization code flow. The client application redirects the user to the `/authorize` endpoint of the authentication server with the required parameters (`response_type=code`, `client_id`, `redirect_uri`, `scope`, etc.).
|
||||
|
||||
## 2. Handle the Token Response
|
||||
|
||||
After the user authenticates with the third-party provider and is redirected back to the client, the client will have an `authorization_code`. The client then exchanges this code for tokens at the `/token` endpoint.
|
||||
|
||||
The response from the `/token` endpoint will differ for new and existing users.
|
||||
|
||||
### For Existing Users
|
||||
|
||||
If the user already has an account, the token response will be a standard OIDC token response, containing:
|
||||
- `access_token`
|
||||
- `id_token`
|
||||
- `refresh_token`
|
||||
- `expires_in`
|
||||
- `token_type: "Bearer"`
|
||||
|
||||
The client should proceed with the standard login flow.
|
||||
|
||||
### For New Users
|
||||
|
||||
If the user is new, the token response will contain a special `onboarding_token`:
|
||||
- `onboarding_token`: A JWT containing information about the new user from the external provider.
|
||||
- `token_type: "Onboarding"`
|
||||
|
||||
The presence of the `onboarding_token` is the signal for the client to start the new user onboarding flow.
|
||||
|
||||
## 3. Process the Onboarding Token
|
||||
|
||||
The `onboarding_token` is a JWT. The client should decode it to access the claims, which will include:
|
||||
|
||||
- `provider`: The name of the external provider (e.g., "Google", "Facebook").
|
||||
- `provider_user_id`: The user's unique ID from the external provider.
|
||||
- `email`: The user's email address (if available).
|
||||
- `name`: The user's full name from the external provider (if available).
|
||||
- `nonce`: The nonce from the initial authorization request.
|
||||
|
||||
Using this information, the client can now guide the user through a custom onboarding process. For example, it can pre-fill a registration form with the user's name and email, and prompt the user to choose a unique username for their new account.
|
||||
|
||||
## 4. Complete the Onboarding
|
||||
|
||||
To finalize the account creation, the client needs to send the collected information to the server. This requires a new API endpoint on the server that is not part of this change.
|
||||
|
||||
**Example Endpoint:** `POST /api/account/onboard`
|
||||
|
||||
The client would send a request to this endpoint, including:
|
||||
- The `onboarding_token`.
|
||||
- The username chosen by the user.
|
||||
- Any other required information.
|
||||
|
||||
The server will validate the `onboarding_token` and create a new user account with the provided details.
|
||||
|
||||
## 5. Finalize Login
|
||||
|
||||
Upon successful account creation, the server's onboarding endpoint should return a standard set of OIDC tokens (`access_token`, `id_token`, `refresh_token`) for the newly created user.
|
||||
|
||||
The client can then use these tokens to log the user in, completing the onboarding and login process.
|
||||
324
docs/PRESENCE_ACTIVITY_API.md
Normal file
324
docs/PRESENCE_ACTIVITY_API.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# Presence Activity API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Presence Activity API allows users to manage their current activities (e.g., gaming, music, workouts) with automatic expiration through a lease-based system. Activities can be created, updated, and deleted, with support for flexible metadata and both system-generated and user-defined identifiers.
|
||||
|
||||
This service is handled by the DysonNetwork.Pass, when using with the gateway, replace the `/api` with the `/pass`
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Lease-Based Expiration**: Activities automatically expire within 1-60 minutes unless renewed
|
||||
- **Flexible Identity**: Support for both autogenerated GUIDs and user-defined ManualIds
|
||||
- **Extensible Metadata**: JSON-stored metadata dictionary for custom developer data
|
||||
- **Soft Deletion**: Activities are soft-deleted and filtered automatically
|
||||
- **Performance Optimized**: Cached active activities with 1-minute expiration
|
||||
- **Authentication Required**: All endpoints require valid user authentication
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Base URL: `/api/activities`
|
||||
|
||||
### Authentication
|
||||
All endpoints require `[Authorize]` header. User context is automatically extracted.
|
||||
|
||||
---
|
||||
|
||||
## Get Active Activities
|
||||
|
||||
Retrieve all currently active (non-expired) presence activities for the authenticated user.
|
||||
|
||||
**Endpoint:** `GET /api/activities`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"type": "Gaming",
|
||||
"manualId": "game-session-1",
|
||||
"title": "Playing Cyberpunk 2077",
|
||||
"subtitle": "Night City Exploration",
|
||||
"caption": "Missions completed: 15",
|
||||
"meta": {
|
||||
"appName": "Cyberpunk 2077",
|
||||
"platform": "Steam",
|
||||
"customProperty": "additional data"
|
||||
},
|
||||
"leaseMinutes": 10,
|
||||
"leaseExpiresAt": "2024-01-15T14:30:00Z",
|
||||
"accountId": "user-guid",
|
||||
"createdAt": "2024-01-15T14:25:00Z",
|
||||
"updatedAt": "2024-01-15T14:25:00Z",
|
||||
"deletedAt": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Common Response Codes:**
|
||||
- `200 OK` - Success, returns array of active activities
|
||||
- `401 Unauthorized` - Invalid or missing authentication
|
||||
|
||||
---
|
||||
|
||||
## Create New Activity
|
||||
|
||||
Create a new presence activity with a configurable lease period.
|
||||
|
||||
**Endpoint:** `POST /api/activities`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"type": "Gaming",
|
||||
"manualId": "my-game-session",
|
||||
"title": "Playing Cyberpunk 2077",
|
||||
"subtitle": "Night City Mission",
|
||||
"caption": "Currently exploring downtown",
|
||||
"meta": {
|
||||
"appName": "Cyberpunk 2077",
|
||||
"platform": "Steam",
|
||||
"difficulty": "Hard",
|
||||
"mods": ["mod1", "mod2"]
|
||||
},
|
||||
"leaseMinutes": 15
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Returns the created `SnPresenceActivity` object with populated fields.
|
||||
|
||||
**Field Details:**
|
||||
- `type`: PresenceType enum (Unknown, Gaming, Music, Workout)
|
||||
- `manualId`: Optional user-defined string identifier
|
||||
- `title`, `subtitle`, `caption`: Display strings (max 4096 chars each)
|
||||
- `meta`: Optional `Dictionary<string, object>` for custom data
|
||||
- `leaseMinutes`: 1-60 minutes (default: 5)
|
||||
|
||||
**Response Codes:**
|
||||
- `200 OK` - Activity created successfully
|
||||
- `400 Bad Request` - Invalid lease minutes or malformed data
|
||||
- `401 Unauthorized` - Invalid authentication
|
||||
|
||||
---
|
||||
|
||||
## Update Activity
|
||||
|
||||
Update an existing activity using either its GUID or ManualId. Only provided fields are updated.
|
||||
|
||||
**Endpoint:** `PUT /api/activities`
|
||||
|
||||
**Query Parameters:** (one required)
|
||||
- `id` - System-generated GUID (string)
|
||||
- `manualId` - User-defined identifier (string)
|
||||
|
||||
**Request Body:** (all fields optional)
|
||||
```json
|
||||
{
|
||||
"title": "Updated: Playing Cyberpunk 2077",
|
||||
"meta": {
|
||||
"appName": "Cyberpunk 2077",
|
||||
"platform": "Steam",
|
||||
"newProperty": "updated data"
|
||||
},
|
||||
"leaseMinutes": 20
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Returns the updated `SnPresenceActivity` object.
|
||||
|
||||
**Response Codes:**
|
||||
- `200 OK` - Activity updated successfully
|
||||
- `400 Bad Request` - Missing or invalid ID parameters
|
||||
- `401 Unauthorized` - Invalid authentication
|
||||
- `404 Not Found` - Activity not found or doesn't belong to user
|
||||
|
||||
**Example cURL:**
|
||||
```bash
|
||||
# Update by ManualId
|
||||
curl -X PUT "/api/activities?manualId=my-game-session" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"leaseMinutes": 20}'
|
||||
|
||||
# Update by GUID
|
||||
curl -X PUT "/api/activities?id=550e8400-e29b-41d4-a716-446655440000" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "Updated Title"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Delete Activity
|
||||
|
||||
Soft-delete an activity using either GUID or ManualId.
|
||||
|
||||
**Endpoint:** `DELETE /api/activities`
|
||||
|
||||
**Query Parameters:** (one required)
|
||||
- `id` - System-generated GUID (string)
|
||||
- `manualId` - User-defined identifier (string)
|
||||
|
||||
**Request Body:** None
|
||||
|
||||
**Response:** No content (204)
|
||||
|
||||
**Response Codes:**
|
||||
- `204 No Content` - Activity deleted successfully
|
||||
- `400 Bad Request` - Missing or invalid ID parameters
|
||||
- `401 Unauthorized` - Invalid authentication
|
||||
- `404 Not Found` - Activity not found or doesn't belong to user
|
||||
|
||||
**Example cURL:**
|
||||
```bash
|
||||
# Delete by ManualId
|
||||
curl -X DELETE "/api/activities?manualId=my-game-session" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Endpoint
|
||||
|
||||
### Get Activities by Account ID
|
||||
|
||||
**Endpoint:** `GET /api/activities/{accountId:guid}`
|
||||
|
||||
For administrative or debugging purposes. Returns activities for the specified account ID, regardless of authentication.
|
||||
|
||||
---
|
||||
|
||||
## Data Models
|
||||
|
||||
### PresenceType Enum
|
||||
```csharp
|
||||
public enum PresenceType
|
||||
{
|
||||
Unknown,
|
||||
Gaming,
|
||||
Music,
|
||||
Workout
|
||||
}
|
||||
```
|
||||
|
||||
### SnPresenceActivity
|
||||
```csharp
|
||||
public class SnPresenceActivity : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } // System-generated GUID
|
||||
public PresenceType Type { get; set; }
|
||||
public string? ManualId { get; set; } // User-defined ID
|
||||
public string? Title { get; set; }
|
||||
public string? Subtitle { get; set; }
|
||||
public string? Caption { get; set; }
|
||||
public Dictionary<string, object>? Meta { get; set; } // JSON metadata
|
||||
public int LeaseMinutes { get; set; } // Lease duration
|
||||
public Instant LeaseExpiresAt { get; set; } // Expiration timestamp
|
||||
|
||||
// Inherited from ModelBase
|
||||
public Guid AccountId { get; set; }
|
||||
public Instant CreatedAt { get; set; }
|
||||
public Instant UpdatedAt { get; set; }
|
||||
public Instant? DeletedAt { get; set; } // Soft deletion
|
||||
}
|
||||
```
|
||||
|
||||
## Behavior & Constraints
|
||||
|
||||
### Lease Expiration
|
||||
- Activities automatically expire when `SystemClock.Instance.GetCurrentInstant() > LeaseExpiresAt`
|
||||
- Expiry is checked in database queries, so expired activities are filtered out of GET operations
|
||||
- Clients must periodically update/renew leases to keep activities active
|
||||
|
||||
### ID Flexibility
|
||||
- **ManualId**: User-defined string, unique within a user's activities
|
||||
- **GUID**: System-generated, always unique, returned in API responses
|
||||
- Both can be used interchangeably for updates and deletion
|
||||
|
||||
### Performance Optimizations
|
||||
- Active activities are cached for 1 minute to handle frequent updates
|
||||
- Cache is invalidated on create/update/delete operations
|
||||
- Database queries filter expired activities automatically
|
||||
|
||||
### Security
|
||||
- All operations are scoped to the authenticated user's account
|
||||
- Users can only manage their own activities
|
||||
- Invalid or expired authentication tokens return 401 Unauthorized
|
||||
|
||||
### Data Storage
|
||||
- Activities are stored in PostgreSQL with JSONB metadata support
|
||||
- Soft deletion uses timestamp rather than hard removal
|
||||
- EF Core middleware automatically handles CreatedAt/UpdatedAt timestamps
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Gaming Session Management
|
||||
```javascript
|
||||
// Start gaming session
|
||||
const activity = await fetch('/api/activities', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: 'Gaming',
|
||||
manualId: 'game-session-1',
|
||||
title: 'Playing Cyberpunk 2077',
|
||||
meta: { appId: 'cyberpunk2077', mods: ['photorealistic'] },
|
||||
leaseMinutes: 15
|
||||
})
|
||||
});
|
||||
|
||||
// Update progress (extend lease)
|
||||
await fetch('/api/activities?manualId=game-session-1', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
title: 'Playing Cyberpunk 2077 - Level 25',
|
||||
leaseMinutes: 15
|
||||
})
|
||||
});
|
||||
|
||||
// End session
|
||||
await fetch('/api/activities?manualId=game-session-1', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
```
|
||||
|
||||
### Metadata Extension
|
||||
```javascript
|
||||
// Rich metadata support
|
||||
const activity = {
|
||||
type: 'Music',
|
||||
manualId: 'spotify-session',
|
||||
title: 'Listening to Electronic',
|
||||
meta: {
|
||||
spotifyTrackId: '1Je1IMUlBXcx1FzbcXRuWw',
|
||||
artist: 'Purity Ring',
|
||||
album: 'Shrines',
|
||||
duration: 240000, // milliseconds
|
||||
custom: { userRating: 5, genre: 'Electronic' }
|
||||
},
|
||||
leaseMinutes: 30
|
||||
};
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Common error responses follow REST API conventions:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Microsoft.AspNetCore.Mvc.ValidationProblemDetails",
|
||||
"title": "One or more validation errors occurred.",
|
||||
"status": 400,
|
||||
"errors": {
|
||||
"leaseMinutes": ["Lease minutes must be between 1 and 60."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Built with ASP.NET Core and Entity Framework Core
|
||||
- Uses NodaTime for precise timestamp handling
|
||||
- PostgreSQL JSONB for flexible metadata storage
|
||||
- Integration with existing authentication and caching systems
|
||||
- Follows established project patterns for soft deletion and audit trails
|
||||
429
docs/WALLET_FUNDS.md
Normal file
429
docs/WALLET_FUNDS.md
Normal file
@@ -0,0 +1,429 @@
|
||||
# Wallet Funds (Red Packet) System
|
||||
|
||||
## Overview
|
||||
|
||||
The Wallet Funds system implements red packet functionality for the DysonNetwork platform, allowing users to create funds that can be split among multiple recipients. Recipients must explicitly claim their portion, and unclaimed funds are automatically refunded after expiration.
|
||||
|
||||
## Features
|
||||
|
||||
- **Red Packet Creation**: Users can create funds with total amounts to be distributed
|
||||
- **Split Types**: Even distribution or random (lucky draw) splitting
|
||||
- **Claim System**: Recipients must actively claim their portion
|
||||
- **Expiration**: Automatic refund of unclaimed funds after 24 hours
|
||||
- **Multi-Recipient**: Support for distributing to multiple users simultaneously
|
||||
- **Audit Trail**: Full transaction history and status tracking
|
||||
|
||||
## Architecture
|
||||
|
||||
### Models
|
||||
|
||||
#### SnWalletFund
|
||||
```csharp
|
||||
public class SnWalletFund : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public decimal TotalAmount { get; set; }
|
||||
public FundSplitType SplitType { get; set; }
|
||||
public FundStatus Status { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public Guid CreatorAccountId { get; set; }
|
||||
public SnAccount CreatorAccount { get; set; }
|
||||
public ICollection<SnWalletFundRecipient> Recipients { get; set; }
|
||||
public Instant ExpiredAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### SnWalletFundRecipient
|
||||
```csharp
|
||||
public class SnWalletFundRecipient : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid FundId { get; set; }
|
||||
public SnWalletFund Fund { get; set; }
|
||||
public Guid RecipientAccountId { get; set; }
|
||||
public SnAccount RecipientAccount { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public bool IsReceived { get; set; }
|
||||
public Instant? ReceivedAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### Enums
|
||||
|
||||
#### FundSplitType
|
||||
- `Even`: Equal distribution among all recipients
|
||||
- `Random`: Random amounts that sum to total
|
||||
|
||||
#### FundStatus
|
||||
- `Created`: Fund created, waiting for claims
|
||||
- `PartiallyReceived`: Some recipients have claimed
|
||||
- `FullyReceived`: All recipients have claimed
|
||||
- `Expired`: Fund expired, unclaimed amounts refunded
|
||||
- `Refunded`: Fund was refunded (legacy status)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Create Fund
|
||||
**POST** `/api/wallets/funds`
|
||||
|
||||
Creates a new fund (red packet) for distribution among recipients.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"recipientAccountIds": ["uuid1", "uuid2", "uuid3"],
|
||||
"currency": "points",
|
||||
"totalAmount": 100.00,
|
||||
"splitType": "Even",
|
||||
"message": "Happy Birthday! 🎉",
|
||||
"expirationHours": 48
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `SnWalletFund` object
|
||||
|
||||
**Authorization:** Required (authenticated user becomes the creator)
|
||||
|
||||
---
|
||||
|
||||
### Get Funds
|
||||
**GET** `/api/wallets/funds`
|
||||
|
||||
Retrieves funds that the authenticated user is involved in (as creator or recipient).
|
||||
|
||||
**Query Parameters:**
|
||||
- `offset` (int, optional): Pagination offset (default: 0)
|
||||
- `take` (int, optional): Number of items to return (default: 20)
|
||||
- `status` (FundStatus, optional): Filter by fund status
|
||||
|
||||
**Response:** Array of `SnWalletFund` objects with `X-Total` header
|
||||
|
||||
**Authorization:** Required
|
||||
|
||||
---
|
||||
|
||||
### Get Fund
|
||||
**GET** `/api/wallets/funds/{id}`
|
||||
|
||||
Retrieves details of a specific fund.
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (Guid): Fund ID
|
||||
|
||||
**Response:** `SnWalletFund` object with recipients
|
||||
|
||||
**Authorization:** Required (user must be creator or recipient)
|
||||
|
||||
---
|
||||
|
||||
### Receive Fund
|
||||
**POST** `/api/wallets/funds/{id}/receive`
|
||||
|
||||
Claims the authenticated user's portion of a fund.
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (Guid): Fund ID
|
||||
|
||||
**Response:** `SnWalletTransaction` object
|
||||
|
||||
**Authorization:** Required (user must be a recipient)
|
||||
|
||||
## Service Methods
|
||||
|
||||
### Creating a Fund
|
||||
|
||||
```csharp
|
||||
// Service method
|
||||
public async Task<SnWalletFund> CreateFundAsync(
|
||||
Guid creatorAccountId,
|
||||
List<Guid> recipientAccountIds,
|
||||
string currency,
|
||||
decimal totalAmount,
|
||||
FundSplitType splitType,
|
||||
string? message = null,
|
||||
Duration? expiration = null)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `creatorAccountId`: Account ID of the fund creator
|
||||
- `recipientAccountIds`: List of recipient account IDs
|
||||
- `currency`: Currency type (e.g., "points", "golds")
|
||||
- `totalAmount`: Total amount to distribute
|
||||
- `splitType`: How to split the amount (Even/Random)
|
||||
- `message`: Optional message for the fund
|
||||
- `expiration`: Optional expiration duration (default: 24 hours)
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var fund = await paymentService.CreateFundAsync(
|
||||
creatorId: userId,
|
||||
recipientAccountIds: new List<Guid> { friend1Id, friend2Id, friend3Id },
|
||||
currency: "points",
|
||||
totalAmount: 100.00m,
|
||||
splitType: FundSplitType.Even,
|
||||
message: "Happy New Year!",
|
||||
expiration: Duration.FromHours(48) // Optional: 48 hours instead of default 24
|
||||
);
|
||||
```
|
||||
|
||||
### Claiming a Fund
|
||||
|
||||
```csharp
|
||||
// Service method
|
||||
public async Task<SnWalletTransaction> ReceiveFundAsync(
|
||||
Guid recipientAccountId,
|
||||
Guid fundId)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `recipientAccountId`: Account ID of the recipient claiming the fund
|
||||
- `fundId`: ID of the fund to claim from
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
var transaction = await paymentService.ReceiveFundAsync(
|
||||
recipientAccountId: myAccountId,
|
||||
fundId: fundId
|
||||
);
|
||||
```
|
||||
|
||||
## Split Logic
|
||||
|
||||
### Even Split
|
||||
Distributes the total amount equally among all recipients, handling decimal precision properly:
|
||||
|
||||
```csharp
|
||||
private List<decimal> SplitEvenly(decimal totalAmount, int recipientCount)
|
||||
{
|
||||
var baseAmount = Math.Floor(totalAmount / recipientCount * 100) / 100;
|
||||
var remainder = totalAmount - (baseAmount * recipientCount);
|
||||
|
||||
var amounts = new List<decimal>();
|
||||
for (int i = 0; i < recipientCount; i++)
|
||||
{
|
||||
var amount = baseAmount;
|
||||
if (i < remainder * 100)
|
||||
amount += 0.01m; // Distribute remainder as 0.01 increments
|
||||
amounts.Add(amount);
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
```
|
||||
|
||||
**Example:** 100.00 split among 3 recipients = [33.34, 33.33, 33.33]
|
||||
|
||||
### Random Split
|
||||
Generates random amounts that sum exactly to the total:
|
||||
|
||||
```csharp
|
||||
private List<decimal> SplitRandomly(decimal totalAmount, int recipientCount)
|
||||
{
|
||||
var random = new Random();
|
||||
var amounts = new List<decimal>();
|
||||
decimal remaining = totalAmount;
|
||||
|
||||
for (int i = 0; i < recipientCount - 1; i++)
|
||||
{
|
||||
var maxAmount = remaining - (recipientCount - i - 1) * 0.01m;
|
||||
var minAmount = 0.01m;
|
||||
var amount = Math.Round((decimal)random.NextDouble() * (maxAmount - minAmount) + minAmount, 2);
|
||||
amounts.Add(amount);
|
||||
remaining -= amount;
|
||||
}
|
||||
|
||||
amounts.Add(Math.Round(remaining, 2)); // Last recipient gets remainder
|
||||
return amounts;
|
||||
}
|
||||
```
|
||||
|
||||
**Example:** 100.00 split randomly among 3 recipients = [45.67, 23.45, 30.88]
|
||||
|
||||
## Expiration and Refunds
|
||||
|
||||
### Automatic Processing
|
||||
Funds are processed hourly by the `FundExpirationJob`:
|
||||
|
||||
```csharp
|
||||
public async Task ProcessExpiredFundsAsync()
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var expiredFunds = await db.WalletFunds
|
||||
.Include(f => f.Recipients)
|
||||
.Where(f => f.Status == FundStatus.Created || f.Status == FundStatus.PartiallyReceived)
|
||||
.Where(f => f.ExpiredAt < now)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var fund in expiredFunds)
|
||||
{
|
||||
var unclaimedAmount = fund.Recipients
|
||||
.Where(r => !r.IsReceived)
|
||||
.Sum(r => r.Amount);
|
||||
|
||||
if (unclaimedAmount > 0)
|
||||
{
|
||||
// Refund to creator
|
||||
var creatorWallet = await wat.GetWalletAsync(fund.CreatorAccountId);
|
||||
if (creatorWallet != null)
|
||||
{
|
||||
await CreateTransactionAsync(
|
||||
payerWalletId: null,
|
||||
payeeWalletId: creatorWallet.Id,
|
||||
currency: fund.Currency,
|
||||
amount: unclaimedAmount,
|
||||
remarks: $"Refund for expired fund {fund.Id}",
|
||||
type: TransactionType.System,
|
||||
silent: true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fund.Status = FundStatus.Expired;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
```
|
||||
|
||||
### Expiration Rules
|
||||
- Default expiration: 24 hours from creation
|
||||
- Custom expiration can be set when creating the fund
|
||||
- Only funds with status `Created` or `PartiallyReceived` are processed
|
||||
- Unclaimed amounts are refunded to the creator
|
||||
- Fund status changes to `Expired`
|
||||
|
||||
## Security & Validation
|
||||
|
||||
### Creation Validation
|
||||
- Creator must have sufficient funds
|
||||
- All recipient accounts must exist and have wallets
|
||||
- At least one recipient required
|
||||
- Total amount must be positive
|
||||
- Creator cannot be a recipient (self-transfer not allowed)
|
||||
|
||||
### Claim Validation
|
||||
- Fund must exist and not be expired/refunded
|
||||
- Recipient must be in the recipient list
|
||||
- Recipient can only claim once
|
||||
- Recipient must have a valid wallet
|
||||
|
||||
### Error Handling
|
||||
- `ArgumentException`: Invalid parameters
|
||||
- `InvalidOperationException`: Business logic violations
|
||||
- All errors provide descriptive messages
|
||||
|
||||
## Database Schema
|
||||
|
||||
### wallet_funds
|
||||
```sql
|
||||
CREATE TABLE wallet_funds (
|
||||
id UUID PRIMARY KEY,
|
||||
currency VARCHAR(128) NOT NULL,
|
||||
total_amount DECIMAL NOT NULL,
|
||||
split_type INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
message TEXT,
|
||||
creator_account_id UUID NOT NULL,
|
||||
expired_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
```
|
||||
|
||||
### wallet_fund_recipients
|
||||
```sql
|
||||
CREATE TABLE wallet_fund_recipients (
|
||||
id UUID PRIMARY KEY,
|
||||
fund_id UUID NOT NULL REFERENCES wallet_funds(id),
|
||||
recipient_account_id UUID NOT NULL,
|
||||
amount DECIMAL NOT NULL,
|
||||
is_received BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
received_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Wallet System
|
||||
- Funds are deducted from creator's wallet pocket immediately upon creation
|
||||
- Individual claims credit recipient's wallet pocket
|
||||
- Refunds credit creator's wallet pocket
|
||||
- All operations create audit transactions
|
||||
|
||||
### Notification System
|
||||
- Integrates with existing push notification system
|
||||
- Notifications sent for fund creation and claims
|
||||
- Uses localized messages for different languages
|
||||
|
||||
### Scheduled Jobs
|
||||
- `FundExpirationJob` runs every hour
|
||||
- Processes expired funds automatically
|
||||
- Handles refunds and status updates
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Red Packet for Group Event
|
||||
```csharp
|
||||
// Create a red packet for 5 friends totaling 500 points
|
||||
var fund = await paymentService.CreateFundAsync(
|
||||
creatorId,
|
||||
friendIds, // List of 5 friend account IDs
|
||||
"points",
|
||||
500.00m,
|
||||
FundSplitType.Random, // Lucky draw
|
||||
"Happy Birthday! 🎉"
|
||||
);
|
||||
```
|
||||
|
||||
### Equal Split Bonus Distribution
|
||||
```csharp
|
||||
// Distribute bonus equally among team members
|
||||
var fund = await paymentService.CreateFundAsync(
|
||||
managerId,
|
||||
teamMemberIds,
|
||||
"golds",
|
||||
1000.00m,
|
||||
FundSplitType.Even,
|
||||
"Monthly performance bonus"
|
||||
);
|
||||
```
|
||||
|
||||
### Claiming a Fund
|
||||
```csharp
|
||||
// User claims their portion
|
||||
try
|
||||
{
|
||||
var transaction = await paymentService.ReceiveFundAsync(userId, fundId);
|
||||
// Success - funds credited to user's wallet
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// Handle error (already claimed, expired, not recipient, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring & Maintenance
|
||||
|
||||
### Key Metrics
|
||||
- Total funds created per period
|
||||
- Claim rate (claimed vs expired)
|
||||
- Average expiration time
|
||||
- Popular split types
|
||||
|
||||
### Cleanup
|
||||
- Soft-deleted records are cleaned up by `AppDatabaseRecyclingJob`
|
||||
- Expired funds are processed by `FundExpirationJob`
|
||||
- No manual intervention required for normal operation
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Fund Templates**: Pre-configured fund types
|
||||
- **Recurring Funds**: Scheduled fund distributions
|
||||
- **Fund Analytics**: Detailed usage statistics
|
||||
- **Fund Categories**: Tagging and categorization
|
||||
- **Bulk Operations**: Create funds for multiple groups
|
||||
- **Fund Forwarding**: Allow recipients to forward unclaimed portions
|
||||
Reference in New Issue
Block a user