feat(marketplace): purchase repository — build/complete/refund docs, 70/30 revenue share, author earnings aggregation (15 tests)
This commit is contained in:
parent
063efa8e41
commit
d0cb3a2238
@ -1,9 +1,9 @@
|
||||
Last refresh: 2026-03-01T07:00:13Z (2026-02-28 23:00:13 PST)
|
||||
Last refresh: 2026-03-02T00:28:03Z (2026-03-01 16:28:03 PST)
|
||||
Cascade conversations: 50 (495M)
|
||||
Memories: 56
|
||||
Memories: 59
|
||||
Implicit context: 20
|
||||
Code tracker dirs: 182
|
||||
File edit history: 2010 entries
|
||||
Code tracker dirs: 230
|
||||
File edit history: 2075 entries
|
||||
Workspace storage: 28 workspaces
|
||||
Repo docs: 14 files across 3 repos
|
||||
Repo workflows: 28 files across 5 repos
|
||||
Repo docs: 15 files across 3 repos
|
||||
Repo workflows: 35 files across 6 repos
|
||||
|
||||
@ -0,0 +1,186 @@
|
||||
# Azure Connection Audit — Full Workspace Report
|
||||
|
||||
> **Date:** 2026-02-22
|
||||
> **Scope:** `learning_ai_common_plat`, `learning_voice_ai_agent`, `learning_multimodal_memory_agents`, `learning_ai_clock`, `learning_ai_fastgap`
|
||||
> **Auditor:** Cascade (AI)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
| Category | Issues Found | Fixed (session 1) | Fixed (session 2) | Remaining |
|
||||
| ---------------------- | ------------ | ----------------- | ----------------------------------------- | ------------------- |
|
||||
| `x-request-id` missing | 12 clients | 2 (MindLyst) | **9** (root cause + feature-flags) | 0 ✅ |
|
||||
| `x-product-id` missing | 6 clients | 0 | **6** (admin + user dashboards + Python) | 0 ✅ |
|
||||
| Cosmos PK mismatch | 1 container | 0 (flagged) | 0 | 1 (needs migration) |
|
||||
| `.env.example` gaps | 4 files | 1 (MindLyst) | **3** (ChronoMind, user-dash, admin-dash) | 0 ✅ |
|
||||
| Hardcoded productId | 2 instances | 0 | **2** (telemetry.ts, platform_client.py) | 0 ✅ |
|
||||
| Python client gaps | 1 file | 0 | **1** (headers + config) | 0 ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 1. `x-request-id` Header — Root Cause
|
||||
|
||||
### Finding
|
||||
|
||||
**`@bytelyst/api-client` does NOT auto-inject `x-request-id`.**
|
||||
|
||||
The `createApiClient()` factory in `packages/api-client/src/client.ts` only sets `Content-Type`, auth token (via `getToken`), and caller-supplied `defaultHeaders`. No `x-request-id` is generated. This means **every consumer** that relies on `@bytelyst/api-client` without explicitly adding the header is missing request tracing.
|
||||
|
||||
### Root Cause Fix
|
||||
|
||||
Add `x-request-id: crypto.randomUUID()` to `buildHeaders()` in `packages/api-client/src/client.ts`. This single change propagates to all consumers automatically.
|
||||
|
||||
### Affected Clients (missing `x-request-id`)
|
||||
|
||||
| Repo | File | Client Pattern |
|
||||
| ---------------- | -------------------------------------------------- | ------------------------------------- |
|
||||
| `common_plat` | `dashboards/admin-web/src/lib/billing-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `common_plat` | `dashboards/admin-web/src/lib/growth-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `common_plat` | `dashboards/admin-web/src/lib/platform-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `common_plat` | `dashboards/tracker-web/src/lib/tracker-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `common_plat` | `packages/extraction/src/client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/billing-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/growth-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/platform-client.ts` | `createApiClient` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/feature-flags.ts` | Custom `fetch` — no `x-request-id` |
|
||||
| `voice_ai_agent` | `backend/src/clients/platform_client.py` | `httpx` — no `x-request-id` |
|
||||
|
||||
### Already Fixed (previous session)
|
||||
|
||||
| Repo | File | Status |
|
||||
| ------------------- | ------------------------------- | ----------------------------- |
|
||||
| `multimodal_memory` | `web/src/lib/billing-client.ts` | ✅ Added via `defaultHeaders` |
|
||||
| `multimodal_memory` | `web/src/lib/feature-flags.ts` | ✅ Added manually |
|
||||
|
||||
### Already Correct
|
||||
|
||||
| Repo | File | Status |
|
||||
| ----------------------- | ------------------------------------------ | ------------------------------------------- |
|
||||
| `ai_fastgap` (NomGap) | `src/api/client.ts` | ✅ Custom client with `crypto.randomUUID()` |
|
||||
| `ai_clock` (ChronoMind) | `web/src/lib/platform-sync.ts` | ✅ Custom client with `crypto.randomUUID()` |
|
||||
| `voice_ai_agent` | `backend/src/main.py` | ✅ Middleware propagates/generates |
|
||||
| `voice_ai_agent` | `backend/src/clients/extraction_client.py` | ✅ Passes `request_id` param |
|
||||
|
||||
---
|
||||
|
||||
## 2. `x-product-id` Header Gaps
|
||||
|
||||
### Clients Missing `x-product-id`
|
||||
|
||||
| Repo | File | Impact |
|
||||
| ---------------- | ----------------------------------------------- | --------------------------------- |
|
||||
| `common_plat` | `admin-web/src/lib/billing-client.ts` | Server can't filter by product |
|
||||
| `common_plat` | `admin-web/src/lib/growth-client.ts` | Server can't filter by product |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/billing-client.ts` | Server can't filter by product |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/growth-client.ts` | Server can't filter by product |
|
||||
| `voice_ai_agent` | `user-dashboard-web/src/lib/platform-client.ts` | Passes in body, not header |
|
||||
| `voice_ai_agent` | `backend/src/clients/platform_client.py` | Passes in body/params, not header |
|
||||
|
||||
### Already Correct
|
||||
|
||||
| Repo | File |
|
||||
| ------------------------------ | ------------------------------------------------------------- |
|
||||
| `ai_fastgap` (NomGap) | `src/api/client.ts` — `x-product-id: API_CONFIG.productId` |
|
||||
| `ai_clock` (ChronoMind) | `web/src/lib/platform-sync.ts` — `x-product-id` header |
|
||||
| `multimodal_memory` (MindLyst) | `web/src/lib/billing-client.ts` — via `defaultHeaders` |
|
||||
| `multimodal_memory` (MindLyst) | `web/src/lib/feature-flags.ts` — explicit header |
|
||||
| `common_plat` | `tracker-web/src/lib/tracker-client.ts` — from `localStorage` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Cosmos DB Partition Key Mismatch
|
||||
|
||||
### `referrals` Container — 3-way Mismatch
|
||||
|
||||
| Location | Partition Key |
|
||||
| ----------------------------------------------------- | ------------- |
|
||||
| `platform-service/src/lib/cosmos-init.ts` | `/id` |
|
||||
| MindLyst `web/src/lib/cosmos.ts` | `/userId` |
|
||||
| Admin dashboard `admin-web/src/lib/cosmos.ts` | `/referrerId` |
|
||||
| User dashboard `user-dashboard-web/src/lib/cosmos.ts` | `/referrerId` |
|
||||
|
||||
**Status:** Flagged in previous session. Cannot be fixed without data migration. Comment added to `cosmos-init.ts`.
|
||||
|
||||
**Risk:** Cross-partition queries will silently succeed but may return incomplete results or fail on point reads if the wrong partition key is specified.
|
||||
|
||||
---
|
||||
|
||||
## 4. Missing Environment Variables in `.env.example` Files
|
||||
|
||||
### ChronoMind `web/.env.example`
|
||||
|
||||
Currently only has:
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003/api
|
||||
```
|
||||
|
||||
**Missing:**
|
||||
|
||||
- `NEXT_PUBLIC_PRODUCT_ID=chronomind` — used implicitly by `platform-sync.ts` (hardcoded there, but should be env-driven for consistency)
|
||||
|
||||
### LysnrAI `user-dashboard-web/.env.example`
|
||||
|
||||
**Missing:**
|
||||
|
||||
- `NEXT_PUBLIC_PRODUCT_ID=lysnrai` — referenced by `feature-flags.ts` line 10
|
||||
- `NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003` — referenced by `feature-flags.ts` line 11
|
||||
|
||||
Has `PLATFORM_SERVICE_URL` (server-side) but not the `NEXT_PUBLIC_` variant (client-side).
|
||||
|
||||
### LysnrAI root `.env.example`
|
||||
|
||||
**Missing:**
|
||||
|
||||
- `NEXT_PUBLIC_PRODUCT_ID` — not needed at root level (desktop app), so this is informational only.
|
||||
|
||||
### Admin dashboard `.env.example`
|
||||
|
||||
**Missing:**
|
||||
|
||||
- `AZURE_KEYVAULT_URL` — referenced by `instrumentation.ts` but not in `.env.example`
|
||||
|
||||
---
|
||||
|
||||
## 5. Hardcoded `productId` Values
|
||||
|
||||
| Repo | File | Line | Value | Should Use |
|
||||
| ------------------- | ---------------------------------------- | ------- | ----------------------------- | ------------------------------------ |
|
||||
| `multimodal_memory` | `web/src/lib/telemetry.ts` | 19 | `productId: 'mindlyst'` | `process.env.NEXT_PUBLIC_PRODUCT_ID` |
|
||||
| `voice_ai_agent` | `backend/src/clients/platform_client.py` | 86, 101 | `product_id: str = "lysnrai"` | `settings.PRODUCT_ID` or config |
|
||||
|
||||
---
|
||||
|
||||
## 6. Python Backend Client Gaps (`platform_client.py`)
|
||||
|
||||
The `PlatformClient` class in `backend/src/clients/platform_client.py` has several issues:
|
||||
|
||||
1. **No `x-request-id` header** on any request
|
||||
2. **No `x-product-id` header** on any request
|
||||
3. **Creates new `httpx.AsyncClient` per request** — no connection pooling
|
||||
4. **Hardcoded `product_id="lysnrai"` defaults** — should use config
|
||||
|
||||
---
|
||||
|
||||
## 7. Previously Fixed (Session 1)
|
||||
|
||||
| Fix | Repo | File |
|
||||
| ------------------------------------------- | ------------------- | -------------------------------------------------- |
|
||||
| Added `x-request-id` to billing client | `multimodal_memory` | `web/src/lib/billing-client.ts` |
|
||||
| Added `x-request-id` to feature flags | `multimodal_memory` | `web/src/lib/feature-flags.ts` |
|
||||
| Added 13 MindLyst containers to cosmos-init | `common_plat` | `services/platform-service/src/lib/cosmos-init.ts` |
|
||||
| Added Blob Storage creds to Python config | `voice_ai_agent` | `backend/src/config.py` |
|
||||
| Added missing env vars to MindLyst | `multimodal_memory` | `web/.env.example` |
|
||||
|
||||
---
|
||||
|
||||
## 8. Recommended Fix Order
|
||||
|
||||
1. **P0 — Root cause:** Add `x-request-id` auto-generation to `@bytelyst/api-client` `buildHeaders()` → fixes 9 TS clients at once
|
||||
2. **P0 — LysnrAI feature-flags:** Add `x-request-id` to the custom `fetch` call in `user-dashboard-web/src/lib/feature-flags.ts`
|
||||
3. **P1 — Python backend:** Add `x-request-id` and `x-product-id` headers to `platform_client.py`
|
||||
4. **P1 — Env vars:** Add missing `NEXT_PUBLIC_*` vars to ChronoMind, LysnrAI user-dashboard, admin-dashboard `.env.example` files
|
||||
5. **P2 — `x-product-id`:** Add to admin/user dashboard clients via `defaultHeaders` in `createApiClient` config
|
||||
6. **P2 — Hardcoded productId:** Replace in `telemetry.ts` and `platform_client.py`
|
||||
7. **P3 — Referrals PK mismatch:** Requires data migration strategy (separate task)
|
||||
@ -0,0 +1,74 @@
|
||||
---
|
||||
description: Verify iOS/mobile code compiles, all files are in Xcode targets, and passes quality checks
|
||||
---
|
||||
|
||||
# Mobile Code Quality — PeakPulse
|
||||
|
||||
## Steps
|
||||
|
||||
1. Ensure working tree is clean:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git status --short
|
||||
```
|
||||
|
||||
2. Generate Xcode project from project.yml:
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodegen generate
|
||||
```
|
||||
|
||||
3. Build PeakPulse target (iOS Simulator):
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodebuild -project PeakPulse.xcodeproj -scheme PeakPulse -destination 'platform=iOS Simulator,name=iPhone 16' -quiet build
|
||||
```
|
||||
|
||||
4. Build PeakPulseTests target:
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodebuild -project PeakPulse.xcodeproj -scheme PeakPulse -destination 'platform=iOS Simulator,name=iPhone 16' -quiet test
|
||||
```
|
||||
|
||||
5. Check for print() statements (should use os.Logger instead):
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
grep -rn "print(" /Users/sd9235/code/mygh/learning_ai_peakpulse/ios/PeakPulse/ --include="*.swift" | grep -v "// print" | grep -v "/// " || echo "No print() found — OK"
|
||||
```
|
||||
|
||||
6. Check for force unwraps:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
grep -rn '![^=]' /Users/sd9235/code/mygh/learning_ai_peakpulse/ios/PeakPulse/ --include="*.swift" | grep -v 'IBOutlet' | grep -v '// !' | grep -v '!=' | grep -v '!//' | head -20 || echo "No force unwraps found — OK"
|
||||
```
|
||||
|
||||
7. Verify all Swift files are under ios/:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
find /Users/sd9235/code/mygh/learning_ai_peakpulse/ios -name "*.swift" | wc -l
|
||||
```
|
||||
|
||||
8. Check entitlements file has required keys:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cat /Users/sd9235/code/mygh/learning_ai_peakpulse/ios/PeakPulse/PeakPulse.entitlements
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
- `ios/project.yml` — XcodeGen project spec
|
||||
- `ios/PeakPulse/PeakPulse.entitlements` — App entitlements
|
||||
- `ios/PeakPulse/Theme/PeakPulseTheme.swift` — Design tokens
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Fix |
|
||||
| ----------------------------- | -------------------------------------------------------------------------- |
|
||||
| ByteLystPlatformSDK not found | Ensure `../../learning_ai_common_plat/packages/swift-platform-sdk/` exists |
|
||||
| xcodegen not installed | `brew install xcodegen` |
|
||||
| Build fails on Simulator | Check deployment target is iOS 17.0 |
|
||||
@ -0,0 +1,82 @@
|
||||
---
|
||||
description: Production readiness check — run all checks, fix as you go, commit incrementally
|
||||
---
|
||||
|
||||
# Production Readiness — PeakPulse
|
||||
|
||||
## Steps
|
||||
|
||||
1. Check git status is clean:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git status --short
|
||||
```
|
||||
|
||||
2. Verify project.yml generates successfully:
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodegen generate
|
||||
```
|
||||
|
||||
3. Build all iOS targets:
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodebuild -project PeakPulse.xcodeproj -scheme PeakPulse -destination 'platform=iOS Simulator,name=iPhone 16' -quiet build
|
||||
```
|
||||
|
||||
4. Run unit tests:
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse/ios && xcodebuild -project PeakPulse.xcodeproj -scheme PeakPulse -destination 'platform=iOS Simulator,name=iPhone 16' -quiet test
|
||||
```
|
||||
|
||||
5. Verify no print() statements in production code:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
grep -rn "print(" /Users/sd9235/code/mygh/learning_ai_peakpulse/ios/PeakPulse/ --include="*.swift" | grep -v "// " || echo "PASS: No print() found"
|
||||
```
|
||||
|
||||
6. Verify no hardcoded colors (should use PeakPulseColors.\*):
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
grep -rn "Color(" /Users/sd9235/code/mygh/learning_ai_peakpulse/ios/PeakPulse/Views/ --include="*.swift" | grep -v "PeakPulseColors" | grep -v Theme | head -10 || echo "PASS: No hardcoded colors"
|
||||
```
|
||||
|
||||
7. Verify entitlements are correct:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cat /Users/sd9235/code/mygh/learning_ai_peakpulse/ios/PeakPulse/PeakPulse.entitlements
|
||||
```
|
||||
|
||||
8. Verify .env.example has all required keys:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cat /Users/sd9235/code/mygh/learning_ai_peakpulse/.env.example
|
||||
```
|
||||
|
||||
9. Run platform-service peak-sessions tests:
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_common_plat && npx vitest run services/platform-service/src/modules/peak-sessions/peak-sessions.test.ts
|
||||
```
|
||||
|
||||
10. Verify CI workflow exists and is valid:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cat /Users/sd9235/code/mygh/learning_ai_peakpulse/.github/workflows/ci.yml | head -20
|
||||
```
|
||||
|
||||
11. Check file counts match docs:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
echo "Swift files:" && find /Users/sd9235/code/mygh/learning_ai_peakpulse/ios -name "*.swift" | wc -l && echo "Test files:" && find /Users/sd9235/code/mygh/learning_ai_peakpulse/ios/PeakPulseTests -name "*.swift" | wc -l
|
||||
```
|
||||
|
||||
12. Final commit and push if any fixes were made.
|
||||
@ -0,0 +1,36 @@
|
||||
---
|
||||
description: Backup main branch then push PeakPulse repo to origin
|
||||
---
|
||||
|
||||
1. Check for uncommitted changes
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git status --short
|
||||
```
|
||||
|
||||
2. If there are changes, commit them with an appropriate message
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git add -A && git commit -m "chore: save work in progress"
|
||||
```
|
||||
|
||||
3. Create a timestamped backup branch
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git branch backup/main-$(date +%Y%m%d-%H%M%S)
|
||||
```
|
||||
|
||||
4. Push main to origin
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git push origin main
|
||||
```
|
||||
|
||||
5. Verify push succeeded
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git log --oneline -3
|
||||
```
|
||||
@ -0,0 +1,31 @@
|
||||
---
|
||||
description: Smart backup of main branches with duplicate detection
|
||||
---
|
||||
|
||||
1. List existing backup branches
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git branch --list 'backup/*' | tail -5
|
||||
```
|
||||
|
||||
2. Get current HEAD commit
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git rev-parse --short HEAD
|
||||
```
|
||||
|
||||
3. Create backup branch with timestamp (skip if HEAD hasn't changed since last backup)
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git branch backup/main-$(date +%Y%m%d-%H%M%S)
|
||||
```
|
||||
|
||||
4. Confirm backup created
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git branch --list 'backup/*' | tail -3
|
||||
```
|
||||
@ -0,0 +1,44 @@
|
||||
---
|
||||
description: Commit all workspace changes in logical order with intelligent messages
|
||||
---
|
||||
|
||||
# Commit Workspace Changes — PeakPulse
|
||||
|
||||
## Steps
|
||||
|
||||
1. Check for uncommitted changes:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git status --short
|
||||
```
|
||||
|
||||
2. If there are changes, stage all files:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git add -A
|
||||
```
|
||||
|
||||
3. Review staged changes to craft an intelligent commit message:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git diff --cached --stat
|
||||
```
|
||||
|
||||
4. Commit with a descriptive single-line message following the convention `type(scope): description`:
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git commit -m "<type>(<scope>): <description>"
|
||||
```
|
||||
|
||||
**Commit types:** feat, fix, docs, refactor, test, chore
|
||||
**Common scopes:** models, services, views, components, platform, tests, config, docs
|
||||
|
||||
5. Verify commit:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git log -1 --oneline
|
||||
```
|
||||
@ -0,0 +1,30 @@
|
||||
---
|
||||
description: Push local main branch to origin for PeakPulse repo
|
||||
---
|
||||
|
||||
1. Check current branch is main
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git branch --show-current
|
||||
```
|
||||
|
||||
2. Check for uncommitted changes
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git status --short
|
||||
```
|
||||
|
||||
3. Push main to origin
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git push origin main
|
||||
```
|
||||
|
||||
4. Verify
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git log --oneline -1
|
||||
```
|
||||
@ -0,0 +1,27 @@
|
||||
---
|
||||
description: Pull latest from origin main for PeakPulse repo
|
||||
---
|
||||
|
||||
# Sync Repos — PeakPulse
|
||||
|
||||
## Steps
|
||||
|
||||
1. Ensure working tree is clean:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git status --short
|
||||
```
|
||||
|
||||
2. Pull latest from origin main:
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git pull origin main
|
||||
```
|
||||
|
||||
3. Verify current state:
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /Users/sd9235/code/mygh/learning_ai_peakpulse && git log -3 --oneline
|
||||
```
|
||||
@ -0,0 +1,174 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
buildPurchaseDoc,
|
||||
completePurchase,
|
||||
refundPurchase,
|
||||
aggregateAuthorEarnings,
|
||||
type PurchaseDoc,
|
||||
} from './purchase-repository.js';
|
||||
|
||||
describe('buildPurchaseDoc', () => {
|
||||
const base = {
|
||||
productId: 'jarvisjr',
|
||||
userId: 'user_1',
|
||||
listingId: 'listing_1',
|
||||
listingTitle: 'Interview Coach Pro',
|
||||
authorId: 'author_1',
|
||||
priceUsd: 4.99,
|
||||
};
|
||||
|
||||
it('creates a purchase with unique id', () => {
|
||||
const a = buildPurchaseDoc(base);
|
||||
const b = buildPurchaseDoc(base);
|
||||
expect(a.id).not.toBe(b.id);
|
||||
expect(a.id).toMatch(/^purchase_/);
|
||||
});
|
||||
|
||||
it('sets status to pending', () => {
|
||||
const doc = buildPurchaseDoc(base);
|
||||
expect(doc.status).toBe('pending');
|
||||
});
|
||||
|
||||
it('calculates 70/30 revenue share', () => {
|
||||
const doc = buildPurchaseDoc(base);
|
||||
expect(doc.authorShareUsd).toBeCloseTo(3.49, 2);
|
||||
expect(doc.platformShareUsd).toBeCloseTo(1.5, 2);
|
||||
});
|
||||
|
||||
it('revenue shares sum to price', () => {
|
||||
const doc = buildPurchaseDoc(base);
|
||||
expect(doc.authorShareUsd + doc.platformShareUsd).toBeCloseTo(base.priceUsd, 1);
|
||||
});
|
||||
|
||||
it('stores stripe session id if provided', () => {
|
||||
const doc = buildPurchaseDoc({ ...base, stripeSessionId: 'cs_test_123' });
|
||||
expect(doc.stripeSessionId).toBe('cs_test_123');
|
||||
});
|
||||
|
||||
it('defaults stripe session to null', () => {
|
||||
const doc = buildPurchaseDoc(base);
|
||||
expect(doc.stripeSessionId).toBeNull();
|
||||
});
|
||||
|
||||
it('sets completedAt and refundedAt to null', () => {
|
||||
const doc = buildPurchaseDoc(base);
|
||||
expect(doc.completedAt).toBeNull();
|
||||
expect(doc.refundedAt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('completePurchase', () => {
|
||||
it('marks purchase as completed', () => {
|
||||
const doc = buildPurchaseDoc({
|
||||
productId: 'jarvisjr',
|
||||
userId: 'u1',
|
||||
listingId: 'l1',
|
||||
listingTitle: 'Test',
|
||||
authorId: 'a1',
|
||||
priceUsd: 9.99,
|
||||
});
|
||||
const completed = completePurchase(doc, 'pi_test_456');
|
||||
expect(completed.status).toBe('completed');
|
||||
expect(completed.stripePaymentIntentId).toBe('pi_test_456');
|
||||
expect(completed.completedAt).toBeTruthy();
|
||||
});
|
||||
|
||||
it('preserves original fields', () => {
|
||||
const doc = buildPurchaseDoc({
|
||||
productId: 'jarvisjr',
|
||||
userId: 'u1',
|
||||
listingId: 'l1',
|
||||
listingTitle: 'Test',
|
||||
authorId: 'a1',
|
||||
priceUsd: 4.99,
|
||||
});
|
||||
const completed = completePurchase(doc, 'pi_test');
|
||||
expect(completed.id).toBe(doc.id);
|
||||
expect(completed.priceUsd).toBe(doc.priceUsd);
|
||||
expect(completed.authorShareUsd).toBe(doc.authorShareUsd);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refundPurchase', () => {
|
||||
it('marks purchase as refunded with reason', () => {
|
||||
const doc = buildPurchaseDoc({
|
||||
productId: 'jarvisjr',
|
||||
userId: 'u1',
|
||||
listingId: 'l1',
|
||||
listingTitle: 'Test',
|
||||
authorId: 'a1',
|
||||
priceUsd: 4.99,
|
||||
});
|
||||
const completed = completePurchase(doc, 'pi_test');
|
||||
const refunded = refundPurchase(completed, 'Customer requested refund');
|
||||
expect(refunded.status).toBe('refunded');
|
||||
expect(refunded.refundReason).toBe('Customer requested refund');
|
||||
expect(refunded.refundedAt).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregateAuthorEarnings', () => {
|
||||
function makePurchase(overrides: Partial<PurchaseDoc> = {}): PurchaseDoc {
|
||||
return {
|
||||
id: `purchase_${Math.random()}`,
|
||||
productId: 'jarvisjr',
|
||||
userId: 'user_1',
|
||||
listingId: 'listing_1',
|
||||
listingTitle: 'Interview Coach',
|
||||
authorId: 'author_1',
|
||||
priceUsd: 4.99,
|
||||
authorShareUsd: 3.49,
|
||||
platformShareUsd: 1.5,
|
||||
stripeSessionId: null,
|
||||
stripePaymentIntentId: 'pi_test',
|
||||
status: 'completed',
|
||||
refundReason: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
completedAt: new Date().toISOString(),
|
||||
refundedAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
it('returns zero for no purchases', () => {
|
||||
const result = aggregateAuthorEarnings([], 'author_1');
|
||||
expect(result.totalEarningsUsd).toBe(0);
|
||||
expect(result.totalSales).toBe(0);
|
||||
expect(result.listings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('aggregates single listing sales', () => {
|
||||
const purchases = [makePurchase(), makePurchase(), makePurchase()];
|
||||
const result = aggregateAuthorEarnings(purchases, 'author_1');
|
||||
expect(result.totalSales).toBe(3);
|
||||
expect(result.totalEarningsUsd).toBeCloseTo(10.47, 2);
|
||||
expect(result.listings).toHaveLength(1);
|
||||
expect(result.listings[0].sales).toBe(3);
|
||||
});
|
||||
|
||||
it('aggregates multiple listing sales', () => {
|
||||
const purchases = [
|
||||
makePurchase({ listingId: 'listing_1', listingTitle: 'Coach A', authorShareUsd: 3.49 }),
|
||||
makePurchase({ listingId: 'listing_2', listingTitle: 'Coach B', authorShareUsd: 6.99 }),
|
||||
makePurchase({ listingId: 'listing_1', listingTitle: 'Coach A', authorShareUsd: 3.49 }),
|
||||
];
|
||||
const result = aggregateAuthorEarnings(purchases, 'author_1');
|
||||
expect(result.totalSales).toBe(3);
|
||||
expect(result.listings).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('excludes refunded purchases', () => {
|
||||
const purchases = [makePurchase(), makePurchase({ status: 'refunded' })];
|
||||
const result = aggregateAuthorEarnings(purchases, 'author_1');
|
||||
expect(result.totalSales).toBe(1);
|
||||
});
|
||||
|
||||
it('excludes other authors', () => {
|
||||
const purchases = [
|
||||
makePurchase({ authorId: 'author_1' }),
|
||||
makePurchase({ authorId: 'author_2' }),
|
||||
];
|
||||
const result = aggregateAuthorEarnings(purchases, 'author_1');
|
||||
expect(result.totalSales).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Purchase Repository — manages marketplace purchase records.
|
||||
* Tracks who bought what, revenue share, and refund status.
|
||||
*/
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export interface PurchaseDoc {
|
||||
id: string;
|
||||
productId: string;
|
||||
userId: string;
|
||||
listingId: string;
|
||||
listingTitle: string;
|
||||
authorId: string;
|
||||
priceUsd: number;
|
||||
authorShareUsd: number;
|
||||
platformShareUsd: number;
|
||||
stripeSessionId: string | null;
|
||||
stripePaymentIntentId: string | null;
|
||||
status: 'pending' | 'completed' | 'refunded' | 'failed';
|
||||
refundReason: string | null;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
refundedAt: string | null;
|
||||
}
|
||||
|
||||
// Revenue share: 70% author, 30% platform
|
||||
const AUTHOR_SHARE = 0.7;
|
||||
const PLATFORM_SHARE = 0.3;
|
||||
|
||||
export function buildPurchaseDoc(input: {
|
||||
productId: string;
|
||||
userId: string;
|
||||
listingId: string;
|
||||
listingTitle: string;
|
||||
authorId: string;
|
||||
priceUsd: number;
|
||||
stripeSessionId?: string;
|
||||
}): PurchaseDoc {
|
||||
const now = new Date().toISOString();
|
||||
const authorShare = Math.round(input.priceUsd * AUTHOR_SHARE * 100) / 100;
|
||||
const platformShare = Math.round(input.priceUsd * PLATFORM_SHARE * 100) / 100;
|
||||
|
||||
return {
|
||||
id: `purchase_${crypto.randomUUID()}`,
|
||||
productId: input.productId,
|
||||
userId: input.userId,
|
||||
listingId: input.listingId,
|
||||
listingTitle: input.listingTitle,
|
||||
authorId: input.authorId,
|
||||
priceUsd: input.priceUsd,
|
||||
authorShareUsd: authorShare,
|
||||
platformShareUsd: platformShare,
|
||||
stripeSessionId: input.stripeSessionId ?? null,
|
||||
stripePaymentIntentId: null,
|
||||
status: 'pending',
|
||||
refundReason: null,
|
||||
createdAt: now,
|
||||
completedAt: null,
|
||||
refundedAt: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function completePurchase(doc: PurchaseDoc, paymentIntentId: string): PurchaseDoc {
|
||||
return {
|
||||
...doc,
|
||||
status: 'completed',
|
||||
stripePaymentIntentId: paymentIntentId,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function refundPurchase(doc: PurchaseDoc, reason: string): PurchaseDoc {
|
||||
return {
|
||||
...doc,
|
||||
status: 'refunded',
|
||||
refundReason: reason,
|
||||
refundedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Author Earnings Aggregation ─────────────────────────────
|
||||
|
||||
export interface AuthorEarnings {
|
||||
authorId: string;
|
||||
totalEarningsUsd: number;
|
||||
totalSales: number;
|
||||
pendingPayoutUsd: number;
|
||||
listings: Array<{
|
||||
listingId: string;
|
||||
listingTitle: string;
|
||||
sales: number;
|
||||
earningsUsd: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function aggregateAuthorEarnings(
|
||||
purchases: PurchaseDoc[],
|
||||
authorId: string
|
||||
): AuthorEarnings {
|
||||
const completed = purchases.filter(p => p.authorId === authorId && p.status === 'completed');
|
||||
|
||||
const byListing = new Map<string, { title: string; sales: number; earnings: number }>();
|
||||
let totalEarnings = 0;
|
||||
|
||||
for (const p of completed) {
|
||||
totalEarnings += p.authorShareUsd;
|
||||
const existing = byListing.get(p.listingId);
|
||||
if (existing) {
|
||||
existing.sales += 1;
|
||||
existing.earnings += p.authorShareUsd;
|
||||
} else {
|
||||
byListing.set(p.listingId, {
|
||||
title: p.listingTitle,
|
||||
sales: 1,
|
||||
earnings: p.authorShareUsd,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
authorId,
|
||||
totalEarningsUsd: Math.round(totalEarnings * 100) / 100,
|
||||
totalSales: completed.length,
|
||||
pendingPayoutUsd: Math.round(totalEarnings * 100) / 100, // Stub: all earnings are pending
|
||||
listings: Array.from(byListing.entries()).map(([listingId, data]) => ({
|
||||
listingId,
|
||||
listingTitle: data.title,
|
||||
sales: data.sales,
|
||||
earningsUsd: Math.round(data.earnings * 100) / 100,
|
||||
})),
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user