mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-09-30 03:03:13 +00:00
Initial scaffold: monorepo structure, CDK stacks, CI/CD, domain model
Phase 0 of proposal-system: complete project setup including: - CDK infrastructure (3 stacks: foundation, compute, frontend) - .NET 8 solution with Clean Architecture (Domain, Application, Infrastructure, Api) - EF Core data model (PostgreSQL) with all entities - Python Lambda placeholders (pdf-extract, pdf-generate, library-ingest) - React 19 web frontend scaffold (Vite + MUI) - React Native mobile placeholder - Shared TypeScript API contracts - GitHub Actions CI/CD (ci.yaml + deploy.yaml) - OIDC deploy role (githubdeploy-proposal-system) - Dependabot configuration - Cognito User Pool with Google OAuth, PKCE clients, groups
This commit is contained in:
commit
0b055b3ad9
48 changed files with 2208 additions and 0 deletions
43
.github/dependabot.yml
vendored
Normal file
43
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /infra
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /web
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: nuget
|
||||
directory: /api
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: /lambdas/pdf-extract
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 3
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: /lambdas/pdf-generate
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 3
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: /lambdas/library-ingest
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 3
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 3
|
||||
76
.github/workflows/ci.yaml
vendored
Normal file
76
.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dotnet:
|
||||
name: .NET Build & Test
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: api
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- run: dotnet restore ProposalSystem.sln
|
||||
- run: dotnet build ProposalSystem.sln --no-restore --configuration Release
|
||||
- run: dotnet test ProposalSystem.sln --no-build --configuration Release
|
||||
|
||||
web:
|
||||
name: Web Frontend Check
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
|
||||
- run: npm ci
|
||||
- run: npx tsc --noEmit
|
||||
|
||||
python:
|
||||
name: Python Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- run: pip install ruff
|
||||
- run: ruff check lambdas/
|
||||
- run: ruff format --check lambdas/
|
||||
|
||||
infra:
|
||||
name: CDK Synth
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: infra
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: infra/package-lock.json
|
||||
|
||||
- run: npm ci
|
||||
- run: npx cdk synth
|
||||
89
.github/workflows/deploy.yaml
vendored
Normal file
89
.github/workflows/deploy.yaml
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to AWS
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: infra/package-lock.json
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
||||
aws-region: us-east-1
|
||||
|
||||
# Build .NET API
|
||||
- name: Build API
|
||||
working-directory: api
|
||||
run: |
|
||||
dotnet publish src/ProposalSystem.Api/ProposalSystem.Api.csproj \
|
||||
--configuration Release \
|
||||
--runtime linux-arm64 \
|
||||
--self-contained false \
|
||||
--output src/ProposalSystem.Api/bin/Release/net8.0/linux-arm64/publish
|
||||
|
||||
# Install Python dependencies for Lambdas
|
||||
- name: Install PDF Extract deps
|
||||
working-directory: lambdas/pdf-extract
|
||||
run: pip install -r requirements.txt -t .
|
||||
|
||||
- name: Install PDF Generate deps
|
||||
working-directory: lambdas/pdf-generate
|
||||
run: pip install -r requirements.txt -t .
|
||||
|
||||
- name: Install Library Ingest deps
|
||||
working-directory: lambdas/library-ingest
|
||||
run: pip install -r requirements.txt -t .
|
||||
|
||||
# CDK Deploy
|
||||
- name: CDK Deploy
|
||||
working-directory: infra
|
||||
run: |
|
||||
npm ci
|
||||
npx cdk deploy --all --require-approval never
|
||||
|
||||
# CloudFront Invalidation (after frontend deploy)
|
||||
- name: Build Web Frontend
|
||||
working-directory: web
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Deploy Web to S3
|
||||
run: |
|
||||
BUCKET=$(aws cloudformation describe-stacks \
|
||||
--stack-name proposal-system-frontend \
|
||||
--query "Stacks[0].Outputs[?OutputKey=='SiteBucketName'].OutputValue" \
|
||||
--output text)
|
||||
aws s3 sync web/dist "s3://$BUCKET" --delete
|
||||
|
||||
- name: Invalidate CloudFront
|
||||
run: |
|
||||
DIST_ID=$(aws cloudformation describe-stacks \
|
||||
--stack-name proposal-system-frontend \
|
||||
--query "Stacks[0].Outputs[?OutputKey=='DistributionId'].OutputValue" \
|
||||
--output text)
|
||||
aws cloudfront create-invalidation --distribution-id "$DIST_ID" --paths "/*"
|
||||
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# .NET
|
||||
**/bin/Debug/
|
||||
**/bin/Release/
|
||||
**/obj/
|
||||
*.user
|
||||
*.suo
|
||||
*.vs/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
.vite/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# CDK
|
||||
cdk.out/
|
||||
|
||||
# SAM
|
||||
.aws-sam/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test
|
||||
coverage/
|
||||
TestResults/
|
||||
|
||||
# Build artifacts
|
||||
*.zip
|
||||
100
README.md
Normal file
100
README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Proposal Management System
|
||||
|
||||
AI-powered proposal generation and management system for Sea Haven Industries. Enables dispatchers to submit proposal requests, uses Bedrock RAG to auto-generate draft line items from historical data, and provides admins with a pricing/approval workspace with professional PDF output.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Backend API | .NET 8 / ASP.NET Core on Lambda (`Amazon.Lambda.AspNetCoreServer`) |
|
||||
| Database | RDS PostgreSQL 15 + EF Core |
|
||||
| Frontend Web | React 19 + MUI v7 + TypeScript |
|
||||
| Mobile | React Native (iOS-first) |
|
||||
| IaC | CDK (TypeScript) - 3 stacks |
|
||||
| AI/RAG | Bedrock Knowledge Base + Claude (Bedrock Runtime) |
|
||||
| PDF Processing | Python 3.12 Lambdas |
|
||||
| Auth | Cognito User Pool + Google OAuth |
|
||||
| Storage | S3 (PDFs, attachments, library) |
|
||||
| Notifications | Slack Bot API |
|
||||
|
||||
## AWS Resources
|
||||
|
||||
- **Stack prefix:** `proposal-system-*`
|
||||
- **Account:** 328440206208
|
||||
- **Region:** us-east-1
|
||||
|
||||
| Resource | Name |
|
||||
|---|---|
|
||||
| Lambda (.NET 8) | `proposal-system-api` |
|
||||
| API Gateway HTTP API | `proposal-system-gateway` |
|
||||
| RDS PostgreSQL | `proposal-system-db` |
|
||||
| S3 Buckets | `proposal-system-uploads`, `proposal-system-generated`, `proposal-system-library` |
|
||||
| Bedrock Knowledge Base | `proposal-system-kb` |
|
||||
| Lambda (Python) | `proposal-system-pdf-extract`, `proposal-system-pdf-generate`, `proposal-system-library-ingest` |
|
||||
| Cognito User Pool | `proposal-system-auth` |
|
||||
| CloudFront | `proposal-system-web` |
|
||||
| SQS Queue | `proposal-system-jobs` + DLQ |
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
proposal-system/
|
||||
├── infra/ # CDK app (TypeScript) - 3 stacks
|
||||
├── api/ # .NET 8 Web API (Lambda-hosted)
|
||||
├── web/ # React 19 + MUI frontend
|
||||
├── mobile/ # React Native app (iOS)
|
||||
├── lambdas/ # Python 3.12 processing functions
|
||||
├── shared/ # Shared TypeScript types/contracts
|
||||
└── .github/ # CI/CD workflows
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 8 SDK
|
||||
- Node.js 24
|
||||
- Python 3.12
|
||||
- AWS CDK CLI (`npm install -g aws-cdk@2.253.1`)
|
||||
|
||||
### Backend API
|
||||
|
||||
```bash
|
||||
cd api
|
||||
dotnet restore
|
||||
dotnet run --project src/ProposalSystem.Api
|
||||
```
|
||||
|
||||
### Web Frontend
|
||||
|
||||
```bash
|
||||
cd web
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Infrastructure
|
||||
|
||||
```bash
|
||||
cd infra
|
||||
npm install
|
||||
npx cdk synth
|
||||
npx cdk deploy --all
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
- **CI:** Runs on pull requests to `main` (dotnet build/test, tsc, ruff, cdk synth)
|
||||
- **Deploy:** Runs on push to `main` (cdk deploy, frontend S3 sync, CloudFront invalidation)
|
||||
- **OIDC Role:** `githubdeploy-proposal-system`
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. Dispatcher submits proposal request (web or mobile)
|
||||
2. API creates proposal record, publishes SQS message
|
||||
3. If vendor PDF attached: `pdf-extract` Lambda parses and structures data
|
||||
4. Suggestion engine queries Bedrock KB for similar proposals, generates line items via Claude
|
||||
5. Admin reviews/edits line items in workspace
|
||||
6. On approval: `pdf-generate` Lambda creates branded PDF
|
||||
7. On send: `library-ingest` Lambda adds to KB for future matching
|
||||
8. Slack notifications at key status transitions
|
||||
37
api/ProposalSystem.sln
Normal file
37
api/ProposalSystem.sln
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProposalSystem.Api", "src\ProposalSystem.Api\ProposalSystem.Api.csproj", "{A1B2C3D4-1111-2222-3333-444455556666}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProposalSystem.Domain", "src\ProposalSystem.Domain\ProposalSystem.Domain.csproj", "{A1B2C3D4-1111-2222-3333-444455557777}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProposalSystem.Application", "src\ProposalSystem.Application\ProposalSystem.Application.csproj", "{A1B2C3D4-1111-2222-3333-444455558888}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProposalSystem.Infrastructure", "src\ProposalSystem.Infrastructure\ProposalSystem.Infrastructure.csproj", "{A1B2C3D4-1111-2222-3333-444455559999}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A1B2C3D4-1111-2222-3333-444455556666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455556666}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455556666}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455556666}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455557777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455557777}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455557777}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455557777}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455558888}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455558888}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455558888}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455558888}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455559999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455559999}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455559999}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1111-2222-3333-444455559999}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
6
api/global.json
Normal file
6
api/global.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"sdk": {
|
||||
"version": "8.0.400",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
11
api/src/ProposalSystem.Api/Controllers/HealthController.cs
Normal file
11
api/src/ProposalSystem.Api/Controllers/HealthController.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ProposalSystem.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class HealthController : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult Get() => Ok(new { status = "healthy", timestamp = DateTime.UtcNow });
|
||||
}
|
||||
32
api/src/ProposalSystem.Api/Program.cs
Normal file
32
api/src/ProposalSystem.Api/Program.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using ProposalSystem.Infrastructure.Data;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
builder.Services.AddDbContext<ProposalDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
builder.Services.AddAuthentication("Bearer")
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.Authority = builder.Configuration["Auth:Authority"];
|
||||
options.Audience = builder.Configuration["Auth:Audience"];
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddDbContextCheck<ProposalDbContext>();
|
||||
|
||||
builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.MapHealthChecks("/api/health");
|
||||
|
||||
app.Run();
|
||||
24
api/src/ProposalSystem.Api/ProposalSystem.Api.csproj
Normal file
24
api/src/ProposalSystem.Api/ProposalSystem.Api.csproj
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
<AWSProjectType>Lambda</AWSProjectType>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ProposalSystem.Application\ProposalSystem.Application.csproj" />
|
||||
<ProjectReference Include="..\ProposalSystem.Infrastructure\ProposalSystem.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Amazon.Lambda.AspNetCoreServer.Hosting" Version="1.7.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
15
api/src/ProposalSystem.Api/appsettings.json
Normal file
15
api/src/ProposalSystem.Api/appsettings.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Auth": {
|
||||
"Authority": "",
|
||||
"Audience": ""
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using ProposalSystem.Domain.Entities;
|
||||
|
||||
namespace ProposalSystem.Application.Interfaces;
|
||||
|
||||
public interface IProposalRepository
|
||||
{
|
||||
Task<Proposal?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<Proposal>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<Proposal> CreateAsync(Proposal proposal, CancellationToken ct = default);
|
||||
Task UpdateAsync(Proposal proposal, CancellationToken ct = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ProposalSystem.Domain\ProposalSystem.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
32
api/src/ProposalSystem.Domain/Entities/AuditLog.cs
Normal file
32
api/src/ProposalSystem.Domain/Entities/AuditLog.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
namespace ProposalSystem.Domain.Entities;
|
||||
|
||||
public enum AuditAction
|
||||
{
|
||||
Submit,
|
||||
Edit,
|
||||
Approve,
|
||||
Download,
|
||||
Regenerate,
|
||||
Revise,
|
||||
AssignAdmin,
|
||||
EditLineItem,
|
||||
EditScope,
|
||||
GeneratePDF,
|
||||
MarkSent,
|
||||
CreateRevision,
|
||||
UpdateRole
|
||||
}
|
||||
|
||||
public class AuditLog
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid? ProposalId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public AuditAction Action { get; set; }
|
||||
public string? Details { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string? IpAddress { get; set; }
|
||||
|
||||
public Proposal? Proposal { get; set; }
|
||||
public User? User { get; set; }
|
||||
}
|
||||
10
api/src/ProposalSystem.Domain/Entities/Customer.cs
Normal file
10
api/src/ProposalSystem.Domain/Entities/Customer.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace ProposalSystem.Domain.Entities;
|
||||
|
||||
public class Customer
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Addresses { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
14
api/src/ProposalSystem.Domain/Entities/GeneratedPdf.cs
Normal file
14
api/src/ProposalSystem.Domain/Entities/GeneratedPdf.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace ProposalSystem.Domain.Entities;
|
||||
|
||||
public class GeneratedPdf
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProposalId { get; set; }
|
||||
public int Revision { get; set; }
|
||||
public string S3Key { get; set; } = string.Empty;
|
||||
public DateTime GeneratedAt { get; set; }
|
||||
public Guid GeneratedById { get; set; }
|
||||
|
||||
public Proposal? Proposal { get; set; }
|
||||
public User? GeneratedBy { get; set; }
|
||||
}
|
||||
34
api/src/ProposalSystem.Domain/Entities/LineItem.cs
Normal file
34
api/src/ProposalSystem.Domain/Entities/LineItem.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
namespace ProposalSystem.Domain.Entities;
|
||||
|
||||
public enum PricingMode
|
||||
{
|
||||
UnitPrice,
|
||||
TotalPrice,
|
||||
Both
|
||||
}
|
||||
|
||||
public enum LineItemSource
|
||||
{
|
||||
AI,
|
||||
Vendor,
|
||||
Manual,
|
||||
Historical
|
||||
}
|
||||
|
||||
public class LineItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProposalId { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; }
|
||||
public string Unit { get; set; } = string.Empty;
|
||||
public decimal? UnitPrice { get; set; }
|
||||
public decimal TotalPrice { get; set; }
|
||||
public PricingMode PricingMode { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public LineItemSource Source { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public Proposal? Proposal { get; set; }
|
||||
}
|
||||
60
api/src/ProposalSystem.Domain/Entities/Proposal.cs
Normal file
60
api/src/ProposalSystem.Domain/Entities/Proposal.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
namespace ProposalSystem.Domain.Entities;
|
||||
|
||||
public enum ProposalStatus
|
||||
{
|
||||
Draft,
|
||||
InReview,
|
||||
Approved,
|
||||
Sent,
|
||||
Revised
|
||||
}
|
||||
|
||||
public enum ServiceCategory
|
||||
{
|
||||
HVAC,
|
||||
Plumbing,
|
||||
Electrical,
|
||||
General,
|
||||
Renovation
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
Standard,
|
||||
Urgent,
|
||||
Emergency
|
||||
}
|
||||
|
||||
public class Proposal
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string ProposalNumber { get; set; } = string.Empty;
|
||||
public string WorkOrderNumber { get; set; } = string.Empty;
|
||||
public string CustomerName { get; set; } = string.Empty;
|
||||
public string CustomerAddress { get; set; } = string.Empty;
|
||||
public string ScopeOfWork { get; set; } = string.Empty;
|
||||
public string? RefinedScope { get; set; }
|
||||
public ServiceCategory ServiceCategory { get; set; }
|
||||
public Priority Priority { get; set; }
|
||||
public ProposalStatus Status { get; set; }
|
||||
public decimal TotalBidAmount { get; set; }
|
||||
public decimal? VendorTotalCost { get; set; }
|
||||
public string Notes { get; set; } = string.Empty;
|
||||
public Guid SubmittedById { get; set; }
|
||||
public DateTime SubmittedAt { get; set; }
|
||||
public Guid? AssignedAdminId { get; set; }
|
||||
public Guid? ApprovedById { get; set; }
|
||||
public DateTime? ApprovedAt { get; set; }
|
||||
public DateTime? SentAt { get; set; }
|
||||
public int CurrentRevision { get; set; } = 1;
|
||||
public Guid? ParentProposalId { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
public User? SubmittedBy { get; set; }
|
||||
public User? AssignedAdmin { get; set; }
|
||||
public User? ApprovedBy { get; set; }
|
||||
public Proposal? ParentProposal { get; set; }
|
||||
public ICollection<LineItem> LineItems { get; set; } = new List<LineItem>();
|
||||
public ICollection<VendorProposal> VendorProposals { get; set; } = new List<VendorProposal>();
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
namespace ProposalSystem.Domain.Entities;
|
||||
|
||||
public class SimilarProposalReference
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProposalId { get; set; }
|
||||
public string ReferencedLibraryItemId { get; set; } = string.Empty;
|
||||
public float SimilarityScore { get; set; }
|
||||
public DateTime ReferencedAt { get; set; }
|
||||
public Guid ReferencedById { get; set; }
|
||||
|
||||
public Proposal? Proposal { get; set; }
|
||||
public User? ReferencedBy { get; set; }
|
||||
}
|
||||
20
api/src/ProposalSystem.Domain/Entities/User.cs
Normal file
20
api/src/ProposalSystem.Domain/Entities/User.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
namespace ProposalSystem.Domain.Entities;
|
||||
|
||||
public enum UserRole
|
||||
{
|
||||
Dispatcher,
|
||||
Admin,
|
||||
SysAdmin
|
||||
}
|
||||
|
||||
public class User
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string CognitoSub { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public UserRole Role { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
24
api/src/ProposalSystem.Domain/Entities/VendorProposal.cs
Normal file
24
api/src/ProposalSystem.Domain/Entities/VendorProposal.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace ProposalSystem.Domain.Entities;
|
||||
|
||||
public enum ProcessingStatus
|
||||
{
|
||||
Pending,
|
||||
Processing,
|
||||
Complete,
|
||||
Failed
|
||||
}
|
||||
|
||||
public class VendorProposal
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProposalId { get; set; }
|
||||
public string VendorName { get; set; } = string.Empty;
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string S3Key { get; set; } = string.Empty;
|
||||
public string? ExtractedData { get; set; }
|
||||
public decimal TotalVendorCost { get; set; }
|
||||
public DateTime UploadedAt { get; set; }
|
||||
public ProcessingStatus ProcessingStatus { get; set; }
|
||||
|
||||
public Proposal? Proposal { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using ProposalSystem.Domain.Entities;
|
||||
|
||||
namespace ProposalSystem.Infrastructure.Data;
|
||||
|
||||
public class ProposalDbContext : DbContext
|
||||
{
|
||||
public ProposalDbContext(DbContextOptions<ProposalDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Proposal> Proposals => Set<Proposal>();
|
||||
public DbSet<LineItem> LineItems => Set<LineItem>();
|
||||
public DbSet<VendorProposal> VendorProposals => Set<VendorProposal>();
|
||||
public DbSet<Customer> Customers => Set<Customer>();
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||
public DbSet<SimilarProposalReference> SimilarProposalReferences => Set<SimilarProposalReference>();
|
||||
public DbSet<GeneratedPdf> GeneratedPdfs => Set<GeneratedPdf>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Proposal>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => e.ProposalNumber).IsUnique();
|
||||
entity.Property(e => e.TotalBidAmount).HasPrecision(18, 2);
|
||||
entity.Property(e => e.VendorTotalCost).HasPrecision(18, 2);
|
||||
entity.Property(e => e.Status).HasConversion<string>();
|
||||
entity.Property(e => e.ServiceCategory).HasConversion<string>();
|
||||
entity.Property(e => e.Priority).HasConversion<string>();
|
||||
|
||||
entity.HasOne(e => e.SubmittedBy).WithMany().HasForeignKey(e => e.SubmittedById).OnDelete(DeleteBehavior.Restrict);
|
||||
entity.HasOne(e => e.AssignedAdmin).WithMany().HasForeignKey(e => e.AssignedAdminId).OnDelete(DeleteBehavior.SetNull);
|
||||
entity.HasOne(e => e.ApprovedBy).WithMany().HasForeignKey(e => e.ApprovedById).OnDelete(DeleteBehavior.SetNull);
|
||||
entity.HasOne(e => e.ParentProposal).WithMany().HasForeignKey(e => e.ParentProposalId).OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<LineItem>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Quantity).HasPrecision(18, 4);
|
||||
entity.Property(e => e.UnitPrice).HasPrecision(18, 2);
|
||||
entity.Property(e => e.TotalPrice).HasPrecision(18, 2);
|
||||
entity.Property(e => e.PricingMode).HasConversion<string>();
|
||||
entity.Property(e => e.Source).HasConversion<string>();
|
||||
|
||||
entity.HasOne(e => e.Proposal).WithMany(p => p.LineItems).HasForeignKey(e => e.ProposalId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<VendorProposal>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.TotalVendorCost).HasPrecision(18, 2);
|
||||
entity.Property(e => e.ProcessingStatus).HasConversion<string>();
|
||||
entity.Property(e => e.ExtractedData).HasColumnType("jsonb");
|
||||
|
||||
entity.HasOne(e => e.Proposal).WithMany(p => p.VendorProposals).HasForeignKey(e => e.ProposalId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Customer>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Addresses).HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<User>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => e.CognitoSub).IsUnique();
|
||||
entity.HasIndex(e => e.Email).IsUnique();
|
||||
entity.Property(e => e.Role).HasConversion<string>();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AuditLog>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Action).HasConversion<string>();
|
||||
entity.Property(e => e.Details).HasColumnType("jsonb");
|
||||
entity.HasIndex(e => e.ProposalId);
|
||||
entity.HasIndex(e => e.Timestamp);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<SimilarProposalReference>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasOne(e => e.Proposal).WithMany().HasForeignKey(e => e.ProposalId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<GeneratedPdf>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasOne(e => e.Proposal).WithMany().HasForeignKey(e => e.ProposalId).OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ProposalSystem.Application\ProposalSystem.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
|
||||
<PackageReference Include="AWSSDK.S3" Version="3.7.405.0" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.401.0" />
|
||||
<PackageReference Include="AWSSDK.SecretsManager" Version="3.7.401.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
37
infra/bin/app.ts
Normal file
37
infra/bin/app.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import * as cdk from 'aws-cdk-lib';
|
||||
import { FoundationStack } from '../lib/foundation-stack';
|
||||
import { ComputeStack } from '../lib/compute-stack';
|
||||
import { FrontendStack } from '../lib/frontend-stack';
|
||||
|
||||
const app = new cdk.App();
|
||||
|
||||
const env: cdk.Environment = {
|
||||
account: '328440206208',
|
||||
region: 'us-east-1',
|
||||
};
|
||||
|
||||
const foundation = new FoundationStack(app, 'proposal-system-foundation', {
|
||||
stackName: 'proposal-system-foundation',
|
||||
env,
|
||||
description: 'Proposal System - VPC, RDS, S3, SQS, Cognito',
|
||||
});
|
||||
|
||||
const compute = new ComputeStack(app, 'proposal-system-compute', {
|
||||
stackName: 'proposal-system-compute',
|
||||
env,
|
||||
description: 'Proposal System - API Lambda, Python Lambdas, Bedrock KB',
|
||||
vpc: foundation.vpc,
|
||||
lambdaSecurityGroup: foundation.lambdaSecurityGroup,
|
||||
dbSecret: foundation.dbSecret,
|
||||
uploadsBucket: foundation.uploadsBucket,
|
||||
generatedBucket: foundation.generatedBucket,
|
||||
libraryBucket: foundation.libraryBucket,
|
||||
jobsQueue: foundation.jobsQueue,
|
||||
userPool: foundation.userPool,
|
||||
});
|
||||
|
||||
new FrontendStack(app, 'proposal-system-frontend', {
|
||||
stackName: 'proposal-system-frontend',
|
||||
env,
|
||||
description: 'Proposal System - CloudFront + S3 web hosting',
|
||||
});
|
||||
10
infra/cdk.context.json
Normal file
10
infra/cdk.context.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"availability-zones:account=328440206208:region=us-east-1": [
|
||||
"us-east-1a",
|
||||
"us-east-1b",
|
||||
"us-east-1c",
|
||||
"us-east-1d",
|
||||
"us-east-1e",
|
||||
"us-east-1f"
|
||||
]
|
||||
}
|
||||
21
infra/cdk.json
Normal file
21
infra/cdk.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
|
||||
"watch": {
|
||||
"include": ["**"],
|
||||
"exclude": [
|
||||
"README.md",
|
||||
"cdk*.json",
|
||||
"**/*.d.ts",
|
||||
"**/*.js",
|
||||
"tsconfig.json",
|
||||
"package*.json",
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
|
||||
"@aws-cdk/core:checkSecretUsage": true,
|
||||
"@aws-cdk/core:target-partitions": ["aws"]
|
||||
}
|
||||
}
|
||||
199
infra/lib/compute-stack.ts
Normal file
199
infra/lib/compute-stack.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
||||
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
||||
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
||||
import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
|
||||
import * as iam from 'aws-cdk-lib/aws-iam';
|
||||
import * as s3 from 'aws-cdk-lib/aws-s3';
|
||||
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
||||
import * as cognito from 'aws-cdk-lib/aws-cognito';
|
||||
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
||||
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
|
||||
import { Construct } from 'constructs';
|
||||
|
||||
export interface ComputeStackProps extends cdk.StackProps {
|
||||
vpc: ec2.IVpc;
|
||||
lambdaSecurityGroup: ec2.ISecurityGroup;
|
||||
dbSecret: secretsmanager.ISecret;
|
||||
uploadsBucket: s3.IBucket;
|
||||
generatedBucket: s3.IBucket;
|
||||
libraryBucket: s3.IBucket;
|
||||
jobsQueue: sqs.IQueue;
|
||||
userPool: cognito.IUserPool;
|
||||
}
|
||||
|
||||
export class ComputeStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props: ComputeStackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
const privateSubnets = { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS };
|
||||
|
||||
// .NET 8 API Lambda
|
||||
const apiFunction = new lambda.Function(this, 'ApiFunction', {
|
||||
functionName: 'proposal-system-api',
|
||||
runtime: lambda.Runtime.DOTNET_8,
|
||||
architecture: lambda.Architecture.ARM_64,
|
||||
handler: 'ProposalSystem.Api',
|
||||
code: lambda.Code.fromAsset('../api/src/ProposalSystem.Api/bin/Release/net8.0/linux-arm64/publish'),
|
||||
memorySize: 1024,
|
||||
timeout: cdk.Duration.seconds(30),
|
||||
vpc: props.vpc,
|
||||
vpcSubnets: privateSubnets,
|
||||
securityGroups: [props.lambdaSecurityGroup],
|
||||
environment: {
|
||||
ASPNETCORE_ENVIRONMENT: 'Production',
|
||||
DB_SECRET_ARN: props.dbSecret.secretArn,
|
||||
UPLOADS_BUCKET: props.uploadsBucket.bucketName,
|
||||
GENERATED_BUCKET: props.generatedBucket.bucketName,
|
||||
LIBRARY_BUCKET: props.libraryBucket.bucketName,
|
||||
JOBS_QUEUE_URL: props.jobsQueue.queueUrl,
|
||||
},
|
||||
tracing: lambda.Tracing.ACTIVE,
|
||||
});
|
||||
|
||||
// API Lambda permissions
|
||||
props.dbSecret.grantRead(apiFunction);
|
||||
props.uploadsBucket.grantReadWrite(apiFunction);
|
||||
props.generatedBucket.grantRead(apiFunction);
|
||||
props.jobsQueue.grantSendMessages(apiFunction);
|
||||
|
||||
apiFunction.addToRolePolicy(new iam.PolicyStatement({
|
||||
actions: ['cognito-idp:AdminGetUser', 'cognito-idp:AdminListGroupsForUser'],
|
||||
resources: [props.userPool.userPoolArn],
|
||||
}));
|
||||
|
||||
// API Gateway HTTP API
|
||||
const httpApi = new apigatewayv2.HttpApi(this, 'HttpApi', {
|
||||
apiName: 'proposal-system-gateway',
|
||||
corsPreflight: {
|
||||
allowOrigins: [
|
||||
'https://proposals.seahaven.com',
|
||||
'http://localhost:5173',
|
||||
],
|
||||
allowMethods: [
|
||||
apigatewayv2.CorsHttpMethod.GET,
|
||||
apigatewayv2.CorsHttpMethod.POST,
|
||||
apigatewayv2.CorsHttpMethod.PUT,
|
||||
apigatewayv2.CorsHttpMethod.DELETE,
|
||||
apigatewayv2.CorsHttpMethod.OPTIONS,
|
||||
],
|
||||
allowHeaders: ['Authorization', 'Content-Type', 'X-Requested-With'],
|
||||
maxAge: cdk.Duration.hours(1),
|
||||
},
|
||||
});
|
||||
|
||||
const apiIntegration = new apigatewayv2Integrations.HttpLambdaIntegration(
|
||||
'ApiIntegration',
|
||||
apiFunction
|
||||
);
|
||||
|
||||
httpApi.addRoutes({
|
||||
path: '/{proxy+}',
|
||||
methods: [apigatewayv2.HttpMethod.ANY],
|
||||
integration: apiIntegration,
|
||||
});
|
||||
|
||||
// Python Lambda: PDF Extract
|
||||
const pdfExtractFunction = new lambda.Function(this, 'PdfExtractFunction', {
|
||||
functionName: 'proposal-system-pdf-extract',
|
||||
runtime: lambda.Runtime.PYTHON_3_12,
|
||||
architecture: lambda.Architecture.ARM_64,
|
||||
handler: 'app.handler',
|
||||
code: lambda.Code.fromAsset('../lambdas/pdf-extract'),
|
||||
memorySize: 1024,
|
||||
timeout: cdk.Duration.seconds(120),
|
||||
vpc: props.vpc,
|
||||
vpcSubnets: privateSubnets,
|
||||
securityGroups: [props.lambdaSecurityGroup],
|
||||
environment: {
|
||||
UPLOADS_BUCKET: props.uploadsBucket.bucketName,
|
||||
API_BASE_URL: httpApi.apiEndpoint,
|
||||
},
|
||||
});
|
||||
|
||||
props.uploadsBucket.grantRead(pdfExtractFunction);
|
||||
pdfExtractFunction.addToRolePolicy(new iam.PolicyStatement({
|
||||
actions: ['bedrock:InvokeModel'],
|
||||
resources: ['*'],
|
||||
}));
|
||||
|
||||
// Python Lambda: PDF Generate
|
||||
const pdfGenerateFunction = new lambda.Function(this, 'PdfGenerateFunction', {
|
||||
functionName: 'proposal-system-pdf-generate',
|
||||
runtime: lambda.Runtime.PYTHON_3_12,
|
||||
architecture: lambda.Architecture.ARM_64,
|
||||
handler: 'app.handler',
|
||||
code: lambda.Code.fromAsset('../lambdas/pdf-generate'),
|
||||
memorySize: 512,
|
||||
timeout: cdk.Duration.seconds(30),
|
||||
vpc: props.vpc,
|
||||
vpcSubnets: privateSubnets,
|
||||
securityGroups: [props.lambdaSecurityGroup],
|
||||
environment: {
|
||||
GENERATED_BUCKET: props.generatedBucket.bucketName,
|
||||
API_BASE_URL: httpApi.apiEndpoint,
|
||||
},
|
||||
});
|
||||
|
||||
props.generatedBucket.grantWrite(pdfGenerateFunction);
|
||||
|
||||
// Python Lambda: Library Ingest
|
||||
const libraryIngestFunction = new lambda.Function(this, 'LibraryIngestFunction', {
|
||||
functionName: 'proposal-system-library-ingest',
|
||||
runtime: lambda.Runtime.PYTHON_3_12,
|
||||
architecture: lambda.Architecture.ARM_64,
|
||||
handler: 'app.handler',
|
||||
code: lambda.Code.fromAsset('../lambdas/library-ingest'),
|
||||
memorySize: 512,
|
||||
timeout: cdk.Duration.seconds(60),
|
||||
vpc: props.vpc,
|
||||
vpcSubnets: privateSubnets,
|
||||
securityGroups: [props.lambdaSecurityGroup],
|
||||
environment: {
|
||||
LIBRARY_BUCKET: props.libraryBucket.bucketName,
|
||||
API_BASE_URL: httpApi.apiEndpoint,
|
||||
},
|
||||
});
|
||||
|
||||
props.libraryBucket.grantWrite(libraryIngestFunction);
|
||||
libraryIngestFunction.addToRolePolicy(new iam.PolicyStatement({
|
||||
actions: ['bedrock:StartIngestionJob'],
|
||||
resources: ['*'],
|
||||
}));
|
||||
|
||||
// SQS Event Source for Python Lambdas
|
||||
// All three consume from the same queue, routed by message attributes
|
||||
// For now, use a single consumer that routes internally
|
||||
// TODO: Phase 4 will refine this to use message filtering or separate queues per function
|
||||
pdfExtractFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
||||
batchSize: 1,
|
||||
filters: [
|
||||
lambda.FilterCriteria.filter({
|
||||
body: { jobType: lambda.FilterRule.isEqual('pdf-extract') },
|
||||
}),
|
||||
],
|
||||
}));
|
||||
|
||||
pdfGenerateFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
||||
batchSize: 1,
|
||||
filters: [
|
||||
lambda.FilterCriteria.filter({
|
||||
body: { jobType: lambda.FilterRule.isEqual('pdf-generate') },
|
||||
}),
|
||||
],
|
||||
}));
|
||||
|
||||
libraryIngestFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
||||
batchSize: 1,
|
||||
filters: [
|
||||
lambda.FilterCriteria.filter({
|
||||
body: { jobType: lambda.FilterRule.isEqual('library-ingest') },
|
||||
}),
|
||||
],
|
||||
}));
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'ApiEndpoint', { value: httpApi.apiEndpoint });
|
||||
new cdk.CfnOutput(this, 'ApiFunctionArn', { value: apiFunction.functionArn });
|
||||
}
|
||||
}
|
||||
282
infra/lib/foundation-stack.ts
Normal file
282
infra/lib/foundation-stack.ts
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
||||
import * as rds from 'aws-cdk-lib/aws-rds';
|
||||
import * as s3 from 'aws-cdk-lib/aws-s3';
|
||||
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
||||
import * as cognito from 'aws-cdk-lib/aws-cognito';
|
||||
import * as logs from 'aws-cdk-lib/aws-logs';
|
||||
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
||||
import { Construct } from 'constructs';
|
||||
|
||||
export class FoundationStack extends cdk.Stack {
|
||||
public readonly vpc: ec2.IVpc;
|
||||
public readonly lambdaSecurityGroup: ec2.ISecurityGroup;
|
||||
public readonly dbSecret: secretsmanager.ISecret;
|
||||
public readonly uploadsBucket: s3.IBucket;
|
||||
public readonly generatedBucket: s3.IBucket;
|
||||
public readonly libraryBucket: s3.IBucket;
|
||||
public readonly jobsQueue: sqs.IQueue;
|
||||
public readonly userPool: cognito.IUserPool;
|
||||
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// VPC: 2 AZs, public + private subnets, single NAT Gateway
|
||||
this.vpc = new ec2.Vpc(this, 'Vpc', {
|
||||
vpcName: 'proposal-system-vpc',
|
||||
maxAzs: 2,
|
||||
natGateways: 1,
|
||||
subnetConfiguration: [
|
||||
{
|
||||
name: 'public',
|
||||
subnetType: ec2.SubnetType.PUBLIC,
|
||||
cidrMask: 24,
|
||||
},
|
||||
{
|
||||
name: 'private',
|
||||
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
||||
cidrMask: 24,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// VPC Endpoints
|
||||
this.vpc.addGatewayEndpoint('S3Endpoint', {
|
||||
service: ec2.GatewayVpcEndpointAwsService.S3,
|
||||
});
|
||||
|
||||
this.vpc.addInterfaceEndpoint('SecretsManagerEndpoint', {
|
||||
service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
|
||||
});
|
||||
|
||||
// Security Groups
|
||||
this.lambdaSecurityGroup = new ec2.SecurityGroup(this, 'LambdaSg', {
|
||||
vpc: this.vpc,
|
||||
securityGroupName: 'proposal-system-lambda-sg',
|
||||
description: 'Security group for proposal system Lambda functions',
|
||||
allowAllOutbound: true,
|
||||
});
|
||||
|
||||
const rdsSg = new ec2.SecurityGroup(this, 'RdsSg', {
|
||||
vpc: this.vpc,
|
||||
securityGroupName: 'proposal-system-rds-sg',
|
||||
description: 'Security group for proposal system RDS instance',
|
||||
allowAllOutbound: false,
|
||||
});
|
||||
|
||||
rdsSg.addIngressRule(
|
||||
this.lambdaSecurityGroup,
|
||||
ec2.Port.tcp(5432),
|
||||
'Allow PostgreSQL from Lambda SG'
|
||||
);
|
||||
|
||||
// RDS PostgreSQL 15
|
||||
const dbInstance = new rds.DatabaseInstance(this, 'Database', {
|
||||
instanceIdentifier: 'proposal-system-db',
|
||||
engine: rds.DatabaseInstanceEngine.postgres({
|
||||
version: rds.PostgresEngineVersion.VER_15,
|
||||
}),
|
||||
instanceType: ec2.InstanceType.of(
|
||||
ec2.InstanceClass.T4G,
|
||||
ec2.InstanceSize.SMALL
|
||||
),
|
||||
vpc: this.vpc,
|
||||
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
|
||||
securityGroups: [rdsSg],
|
||||
multiAz: false,
|
||||
allocatedStorage: 20,
|
||||
maxAllocatedStorage: 100,
|
||||
storageEncrypted: true,
|
||||
backupRetention: cdk.Duration.days(7),
|
||||
deletionProtection: true,
|
||||
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||
databaseName: 'proposals',
|
||||
credentials: rds.Credentials.fromGeneratedSecret('proposalsadmin', {
|
||||
secretName: 'proposal-system/db-credentials',
|
||||
}),
|
||||
publiclyAccessible: false,
|
||||
});
|
||||
|
||||
this.dbSecret = dbInstance.secret!;
|
||||
|
||||
// S3 Buckets
|
||||
this.uploadsBucket = new s3.Bucket(this, 'UploadsBucket', {
|
||||
bucketName: `proposal-system-uploads-${this.account}`,
|
||||
encryption: s3.BucketEncryption.S3_MANAGED,
|
||||
versioned: true,
|
||||
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
||||
lifecycleRules: [
|
||||
{
|
||||
transitions: [
|
||||
{
|
||||
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
|
||||
transitionAfter: cdk.Duration.days(90),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
cors: [
|
||||
{
|
||||
allowedMethods: [s3.HttpMethods.PUT, s3.HttpMethods.POST],
|
||||
allowedOrigins: ['*'],
|
||||
allowedHeaders: ['*'],
|
||||
maxAge: 3600,
|
||||
},
|
||||
],
|
||||
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||
});
|
||||
cdk.Tags.of(this.uploadsBucket).add('Purpose', 'Vendor PDFs and dispatcher attachments');
|
||||
cdk.Tags.of(this.uploadsBucket).add('ManagedBy', 'proposal-system');
|
||||
|
||||
this.generatedBucket = new s3.Bucket(this, 'GeneratedBucket', {
|
||||
bucketName: `proposal-system-generated-${this.account}`,
|
||||
encryption: s3.BucketEncryption.S3_MANAGED,
|
||||
versioned: true,
|
||||
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
||||
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||
});
|
||||
cdk.Tags.of(this.generatedBucket).add('Purpose', 'Generated proposal PDFs');
|
||||
cdk.Tags.of(this.generatedBucket).add('ManagedBy', 'proposal-system');
|
||||
|
||||
this.libraryBucket = new s3.Bucket(this, 'LibraryBucket', {
|
||||
bucketName: `proposal-system-library-${this.account}`,
|
||||
encryption: s3.BucketEncryption.S3_MANAGED,
|
||||
versioned: true,
|
||||
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
||||
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||
});
|
||||
cdk.Tags.of(this.libraryBucket).add('Purpose', 'Historical proposal library for RAG');
|
||||
cdk.Tags.of(this.libraryBucket).add('ManagedBy', 'proposal-system');
|
||||
|
||||
// SQS Queue + DLQ
|
||||
const dlq = new sqs.Queue(this, 'JobsDlq', {
|
||||
queueName: 'proposal-system-jobs-dlq',
|
||||
retentionPeriod: cdk.Duration.days(14),
|
||||
});
|
||||
|
||||
this.jobsQueue = new sqs.Queue(this, 'JobsQueue', {
|
||||
queueName: 'proposal-system-jobs',
|
||||
visibilityTimeout: cdk.Duration.seconds(180),
|
||||
deadLetterQueue: {
|
||||
queue: dlq,
|
||||
maxReceiveCount: 3,
|
||||
},
|
||||
});
|
||||
|
||||
// Cognito User Pool
|
||||
const userPool = new cognito.UserPool(this, 'UserPool', {
|
||||
userPoolName: 'proposal-system-auth',
|
||||
selfSignUpEnabled: false,
|
||||
signInAliases: { email: true },
|
||||
standardAttributes: {
|
||||
email: { required: true, mutable: true },
|
||||
fullname: { required: true, mutable: true },
|
||||
},
|
||||
passwordPolicy: {
|
||||
minLength: 12,
|
||||
requireUppercase: true,
|
||||
requireLowercase: true,
|
||||
requireDigits: true,
|
||||
requireSymbols: false,
|
||||
},
|
||||
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
|
||||
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||
});
|
||||
|
||||
this.userPool = userPool;
|
||||
|
||||
// Cognito Groups
|
||||
new cognito.CfnUserPoolGroup(this, 'DispatchersGroup', {
|
||||
userPoolId: userPool.userPoolId,
|
||||
groupName: 'dispatchers',
|
||||
description: 'Dispatchers who submit proposal requests',
|
||||
});
|
||||
|
||||
new cognito.CfnUserPoolGroup(this, 'AdminsGroup', {
|
||||
userPoolId: userPool.userPoolId,
|
||||
groupName: 'admins',
|
||||
description: 'Admins who review and approve proposals',
|
||||
});
|
||||
|
||||
new cognito.CfnUserPoolGroup(this, 'SysadminsGroup', {
|
||||
userPoolId: userPool.userPoolId,
|
||||
groupName: 'sysadmins',
|
||||
description: 'System administrators',
|
||||
});
|
||||
|
||||
// Cognito Domain
|
||||
userPool.addDomain('CognitoDomain', {
|
||||
cognitoDomain: { domainPrefix: 'proposal-system-seahaven' },
|
||||
});
|
||||
|
||||
// Web App Client (PKCE)
|
||||
userPool.addClient('WebClient', {
|
||||
userPoolClientName: 'proposal-system-web',
|
||||
generateSecret: false,
|
||||
authFlows: {
|
||||
userSrp: true,
|
||||
},
|
||||
oAuth: {
|
||||
flows: { authorizationCodeGrant: true },
|
||||
scopes: [
|
||||
cognito.OAuthScope.OPENID,
|
||||
cognito.OAuthScope.EMAIL,
|
||||
cognito.OAuthScope.PROFILE,
|
||||
],
|
||||
callbackUrls: [
|
||||
'https://proposals.seahaven.com/callback',
|
||||
'http://localhost:5173/callback',
|
||||
],
|
||||
logoutUrls: [
|
||||
'https://proposals.seahaven.com',
|
||||
'http://localhost:5173',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Mobile App Client (PKCE)
|
||||
userPool.addClient('MobileClient', {
|
||||
userPoolClientName: 'proposal-system-mobile',
|
||||
generateSecret: false,
|
||||
authFlows: {
|
||||
userSrp: true,
|
||||
},
|
||||
oAuth: {
|
||||
flows: { authorizationCodeGrant: true },
|
||||
scopes: [
|
||||
cognito.OAuthScope.OPENID,
|
||||
cognito.OAuthScope.EMAIL,
|
||||
cognito.OAuthScope.PROFILE,
|
||||
],
|
||||
callbackUrls: ['proposalsystem://callback'],
|
||||
logoutUrls: ['proposalsystem://logout'],
|
||||
},
|
||||
});
|
||||
|
||||
// CloudWatch Log Groups
|
||||
const logGroupNames = [
|
||||
'proposal-system-api',
|
||||
'proposal-system-pdf-extract',
|
||||
'proposal-system-pdf-generate',
|
||||
'proposal-system-library-ingest',
|
||||
];
|
||||
|
||||
for (const name of logGroupNames) {
|
||||
new logs.LogGroup(this, `LogGroup-${name}`, {
|
||||
logGroupName: `/aws/lambda/${name}`,
|
||||
retention: logs.RetentionDays.TWO_MONTHS,
|
||||
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||
});
|
||||
}
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'VpcId', { value: this.vpc.vpcId });
|
||||
new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
|
||||
new cdk.CfnOutput(this, 'UserPoolArn', { value: userPool.userPoolArn });
|
||||
new cdk.CfnOutput(this, 'UploadsBucketName', { value: this.uploadsBucket.bucketName });
|
||||
new cdk.CfnOutput(this, 'GeneratedBucketName', { value: this.generatedBucket.bucketName });
|
||||
new cdk.CfnOutput(this, 'LibraryBucketName', { value: this.libraryBucket.bucketName });
|
||||
new cdk.CfnOutput(this, 'JobsQueueUrl', { value: this.jobsQueue.queueUrl });
|
||||
new cdk.CfnOutput(this, 'DbSecretArn', { value: this.dbSecret.secretArn });
|
||||
}
|
||||
}
|
||||
49
infra/lib/frontend-stack.ts
Normal file
49
infra/lib/frontend-stack.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import * as cdk from 'aws-cdk-lib';
|
||||
import * as s3 from 'aws-cdk-lib/aws-s3';
|
||||
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
|
||||
import * as cloudfrontOrigins from 'aws-cdk-lib/aws-cloudfront-origins';
|
||||
import { Construct } from 'constructs';
|
||||
|
||||
export class FrontendStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
const siteBucket = new s3.Bucket(this, 'SiteBucket', {
|
||||
bucketName: `proposal-system-web-${this.account}`,
|
||||
encryption: s3.BucketEncryption.S3_MANAGED,
|
||||
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
||||
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||
autoDeleteObjects: true,
|
||||
});
|
||||
|
||||
const distribution = new cloudfront.Distribution(this, 'Distribution', {
|
||||
comment: 'proposal-system-web',
|
||||
defaultBehavior: {
|
||||
origin: cloudfrontOrigins.S3BucketOrigin.withOriginAccessControl(siteBucket),
|
||||
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
||||
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
||||
responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.SECURITY_HEADERS,
|
||||
},
|
||||
defaultRootObject: 'index.html',
|
||||
errorResponses: [
|
||||
{
|
||||
httpStatus: 403,
|
||||
responseHttpStatus: 200,
|
||||
responsePagePath: '/index.html',
|
||||
ttl: cdk.Duration.seconds(0),
|
||||
},
|
||||
{
|
||||
httpStatus: 404,
|
||||
responseHttpStatus: 200,
|
||||
responsePagePath: '/index.html',
|
||||
ttl: cdk.Duration.seconds(0),
|
||||
},
|
||||
],
|
||||
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'DistributionId', { value: distribution.distributionId });
|
||||
new cdk.CfnOutput(this, 'DistributionDomainName', { value: distribution.distributionDomainName });
|
||||
new cdk.CfnOutput(this, 'SiteBucketName', { value: siteBucket.bucketName });
|
||||
}
|
||||
}
|
||||
493
infra/package-lock.json
generated
Normal file
493
infra/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
{
|
||||
"name": "proposal-system-infra",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "proposal-system-infra",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"aws-cdk-lib": "2.253.1",
|
||||
"constructs": "^10.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"aws-cdk": "^2.1122.0",
|
||||
"typescript": "~5.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-cdk/asset-awscli-v1": {
|
||||
"version": "2.2.273",
|
||||
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz",
|
||||
"integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@aws-cdk/asset-node-proxy-agent-v6": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.2.tgz",
|
||||
"integrity": "sha512-pDiuqH+qY3zM9lhhLjbKJ1tnKOHzQ2V4Wr/3qsxyKeKAkuPMI/BVGvZG1PbrikUw949cGVTfVEt4ETKKYnrj0Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@aws-cdk/cloud-assembly-schema": {
|
||||
"version": "53.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.24.0.tgz",
|
||||
"integrity": "sha512-rCwsd0Y9z4CUmV5FhzjbO2MprXjKG0rxCACuLEY40lD+wInvgcHer0lSDo7Xq9Sxe/IoNe/Wxb2YP3GgxvT3GA==",
|
||||
"bundleDependencies": [
|
||||
"jsonschema",
|
||||
"semver"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jsonschema": "~1.4.1",
|
||||
"semver": "^7.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": {
|
||||
"version": "1.4.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": {
|
||||
"version": "7.8.0",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
||||
"integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1122.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1122.0.tgz",
|
||||
"integrity": "sha512-AI2Ks9qioWLvBPD4IoEtTet3wUG/o/q6U3WR3VCQKH5sNYLLPALo8o9sermNpMnfd1OQkqhL20tp4cEyojrIZg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"cdk": "bin/cdk"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib": {
|
||||
"version": "2.253.1",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.253.1.tgz",
|
||||
"integrity": "sha512-vy+hA15/ZfSQpivkNdlIn2ZDA2hesp3WJgmtIZJDFwu6xzwv7wH7glbAdu5xCHGcOjepOaTKZSvCPC6sN+0/Vw==",
|
||||
"bundleDependencies": [
|
||||
"@balena/dockerignore",
|
||||
"@aws-cdk/cloud-assembly-api",
|
||||
"case",
|
||||
"fs-extra",
|
||||
"ignore",
|
||||
"jsonschema",
|
||||
"minimatch",
|
||||
"punycode",
|
||||
"semver",
|
||||
"table",
|
||||
"yaml",
|
||||
"mime-types"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-cdk/asset-awscli-v1": "2.2.273",
|
||||
"@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1",
|
||||
"@aws-cdk/cloud-assembly-api": "^2.2.2",
|
||||
"@aws-cdk/cloud-assembly-schema": "^53.18.0",
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"case": "1.6.3",
|
||||
"fs-extra": "^11.3.3",
|
||||
"ignore": "^5.3.2",
|
||||
"jsonschema": "^1.5.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"minimatch": "^10.2.3",
|
||||
"punycode": "^2.3.1",
|
||||
"semver": "^7.7.4",
|
||||
"table": "^6.9.0",
|
||||
"yaml": "1.10.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"constructs": "^10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": {
|
||||
"version": "2.2.2",
|
||||
"bundleDependencies": [
|
||||
"jsonschema",
|
||||
"semver"
|
||||
],
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jsonschema": "~1.4.1",
|
||||
"semver": "^7.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-cdk/cloud-assembly-schema": ">=53.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": {
|
||||
"version": "1.0.2",
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/ajv": {
|
||||
"version": "8.18.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/astral-regex": {
|
||||
"version": "2.0.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/case": {
|
||||
"version": "1.6.3",
|
||||
"inBundle": true,
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"inBundle": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/fs-extra": {
|
||||
"version": "11.3.3",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"inBundle": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/jsonschema": {
|
||||
"version": "1.5.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/lodash.truncate": {
|
||||
"version": "4.4.2",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"inBundle": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/slice-ansi": {
|
||||
"version": "4.0.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"astral-regex": "^2.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/table": {
|
||||
"version": "6.9.0",
|
||||
"inBundle": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.1",
|
||||
"lodash.truncate": "^4.4.2",
|
||||
"slice-ansi": "^4.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/yaml": {
|
||||
"version": "1.10.3",
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/constructs": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz",
|
||||
"integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
infra/package.json
Normal file
21
infra/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "proposal-system-infra",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
"cdk": "cdk",
|
||||
"synth": "cdk synth",
|
||||
"deploy": "cdk deploy --all --require-approval never"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-cdk-lib": "2.253.1",
|
||||
"constructs": "^10.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"aws-cdk": "^2.1122.0",
|
||||
"typescript": "~5.7.0"
|
||||
}
|
||||
}
|
||||
24
infra/tsconfig.json
Normal file
24
infra/tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"experimentalDecorators": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"exclude": ["node_modules", "cdk.out", "dist"]
|
||||
}
|
||||
14
lambdas/library-ingest/app.py
Normal file
14
lambdas/library-ingest/app.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""Proposal System - Library Ingest Lambda.
|
||||
|
||||
Processes approved proposals into the Bedrock Knowledge Base.
|
||||
Implementation in Phase 4.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
for record in event.get("Records", []):
|
||||
body = json.loads(record["body"])
|
||||
print(f"Processing library-ingest job: {body}")
|
||||
return {"statusCode": 200}
|
||||
1
lambdas/library-ingest/requirements.txt
Normal file
1
lambdas/library-ingest/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
boto3>=1.35.0,<2.0
|
||||
14
lambdas/pdf-extract/app.py
Normal file
14
lambdas/pdf-extract/app.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""Proposal System - PDF Extract Lambda.
|
||||
|
||||
Parses vendor proposal PDFs and extracts structured line item data.
|
||||
Implementation in Phase 4.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
for record in event.get("Records", []):
|
||||
body = json.loads(record["body"])
|
||||
print(f"Processing pdf-extract job: {body}")
|
||||
return {"statusCode": 200}
|
||||
2
lambdas/pdf-extract/requirements.txt
Normal file
2
lambdas/pdf-extract/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pdfplumber>=0.11.0,<1.0
|
||||
boto3>=1.35.0,<2.0
|
||||
14
lambdas/pdf-generate/app.py
Normal file
14
lambdas/pdf-generate/app.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""Proposal System - PDF Generate Lambda.
|
||||
|
||||
Generates professional branded proposal PDFs from approved proposals.
|
||||
Implementation in Phase 5.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
for record in event.get("Records", []):
|
||||
body = json.loads(record["body"])
|
||||
print(f"Processing pdf-generate job: {body}")
|
||||
return {"statusCode": 200}
|
||||
2
lambdas/pdf-generate/requirements.txt
Normal file
2
lambdas/pdf-generate/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
reportlab>=4.2.0,<5.0
|
||||
boto3>=1.35.0,<2.0
|
||||
17
mobile/package.json
Normal file
17
mobile/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "proposal-system-mobile",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "react-native start",
|
||||
"ios": "react-native run-ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-native": "^0.76.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"typescript": "~5.7.0"
|
||||
}
|
||||
}
|
||||
13
shared/api-contracts/package.json
Normal file
13
shared/api-contracts/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "@proposal-system/api-contracts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.7.0"
|
||||
}
|
||||
}
|
||||
61
shared/api-contracts/src/index.ts
Normal file
61
shared/api-contracts/src/index.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
export type ProposalStatus = 'Draft' | 'InReview' | 'Approved' | 'Sent' | 'Revised';
|
||||
export type ServiceCategory = 'HVAC' | 'Plumbing' | 'Electrical' | 'General' | 'Renovation';
|
||||
export type Priority = 'Standard' | 'Urgent' | 'Emergency';
|
||||
export type LineItemSource = 'AI' | 'Vendor' | 'Manual' | 'Historical';
|
||||
export type PricingMode = 'UnitPrice' | 'TotalPrice' | 'Both';
|
||||
export type UserRole = 'Dispatcher' | 'Admin' | 'SysAdmin';
|
||||
|
||||
export interface Proposal {
|
||||
id: string;
|
||||
proposalNumber: string;
|
||||
workOrderNumber: string;
|
||||
customerName: string;
|
||||
customerAddress: string;
|
||||
scopeOfWork: string;
|
||||
refinedScope: string | null;
|
||||
serviceCategory: ServiceCategory;
|
||||
priority: Priority;
|
||||
status: ProposalStatus;
|
||||
totalBidAmount: number;
|
||||
vendorTotalCost: number | null;
|
||||
notes: string;
|
||||
submittedById: string;
|
||||
submittedAt: string;
|
||||
assignedAdminId: string | null;
|
||||
approvedById: string | null;
|
||||
approvedAt: string | null;
|
||||
sentAt: string | null;
|
||||
currentRevision: number;
|
||||
parentProposalId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface LineItem {
|
||||
id: string;
|
||||
proposalId: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
unitPrice: number | null;
|
||||
totalPrice: number;
|
||||
pricingMode: PricingMode;
|
||||
sortOrder: number;
|
||||
source: LineItemSource;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateProposalRequest {
|
||||
workOrderNumber: string;
|
||||
customerName: string;
|
||||
customerAddress: string;
|
||||
scopeOfWork: string;
|
||||
serviceCategory: ServiceCategory;
|
||||
priority: Priority;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateLineItemsRequest {
|
||||
lineItems: Omit<LineItem, 'id' | 'proposalId' | 'createdAt' | 'updatedAt'>[];
|
||||
}
|
||||
12
shared/api-contracts/tsconfig.json
Normal file
12
shared/api-contracts/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
12
web/index.html
Normal file
12
web/index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Proposal System - Sea Haven Industries</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
28
web/package.json
Normal file
28
web/package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "proposal-system-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/material": "^7.0.0",
|
||||
"@tanstack/react-query": "^5.60.0",
|
||||
"axios": "^1.7.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"typescript": "~5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
12
web/src/main.tsx
Normal file
12
web/src/main.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
function App() {
|
||||
return <div>Proposal System</div>;
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
21
web/tsconfig.json
Normal file
21
web/tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
9
web/vite.config.ts
Normal file
9
web/vite.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue