読み込み中
豆知識
領収書から出張報告・経費精算書(必要なら給与・支払)を作るウィザードのバックエンド API 仕様です。いまの標準はステップ 0 の 「支払はあとで合算」 で給与・支払を作らず、月次の合算支払に渡します。AI 解析と書類リンクも含みます。
本文は英語のエンジニア向け仕様です。 画面の使い方は 月次精算の書類の作り方とつなぎ方 とアプリ内の経費ウィザードを見てください。
┌─────────────────────────────────────────────────────────────────────────┐
│ EXPENSE WIZARD FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Frontend Backend External │
│ ──────── ─────── ──────── │
│ │
│ 1. File Upload ──────────────► POST /expense-wizard/analyze │
│ │ │
│ ├─► AI OCR Service │
│ ├─► AI Categorization │
│ ├─► Anomaly Detection │
│ └─► Business Relevance Check │
│ │ │
│ 2. Review Results ◄─────────────────┘ │
│ │
│ 3. Select Docs ──────────────► POST /expense-wizard/preview │
│ │ │
│ └─► Generate Document Previews │
│ │ │
│ 4. Create Docs ◄─────────────────────┘ │
│ │ │
│ └─────────────────────► POST /expense-wizard/create │
│ │ │
│ ├─► Create Documents (atomic) │
│ ├─► Create Relationships │
│ ├─► Link Bank Transaction │
│ └─► Update Audit Log │
│ │ │
│ 5. Success ◄────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
-- These tables already exist and will be used:
-- documents, document_relationships, document_transaction_links,
-- bank_statements, companies, company_allowances
CREATE TABLE expense_wizard_sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- Session State
status VARCHAR(20) NOT NULL DEFAULT 'draft',
-- Values: 'draft', 'analyzing', 'reviewed', 'creating', 'completed', 'failed', 'cancelled'
-- Configuration
trip_type VARCHAR(20) NOT NULL DEFAULT 'travel',
-- Values: 'travel', 'reimbursement', 'other'
selected_doc_types JSONB NOT NULL DEFAULT '["claims"]',
-- Array of: 'travel', 'claims', 'salary_slip', 'payment'
-- Trip Information (optional, for travel type)
trip_info JSONB,
-- Structure: { destination, startDate, endDate, purpose, dailyAllowance }
-- Totals (calculated)
total_amount DECIMAL(15,2) DEFAULT 0,
total_items INTEGER DEFAULT 0,
daily_allowance_total DECIMAL(15,2) DEFAULT 0,
-- Created Documents (after completion)
created_document_ids JSONB DEFAULT '[]',
-- Array of document UUIDs in creation order
-- Error tracking
last_error TEXT,
error_details JSONB,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
-- RLS
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES auth.users(id)
);
-- Indexes
CREATE INDEX idx_wizard_sessions_user ON expense_wizard_sessions(user_id);
CREATE INDEX idx_wizard_sessions_status ON expense_wizard_sessions(status);
CREATE INDEX idx_wizard_sessions_created ON expense_wizard_sessions(created_at DESC);
-- RLS Policies
ALTER TABLE expense_wizard_sessions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage own wizard sessions"
ON expense_wizard_sessions
FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE TABLE expense_wizard_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
session_id UUID NOT NULL REFERENCES expense_wizard_sessions(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- File Information
file_name VARCHAR(500) NOT NULL,
file_type VARCHAR(100),
file_size INTEGER,
file_storage_path TEXT, -- Supabase Storage path
-- Analysis Status
analysis_status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- Values: 'pending', 'analyzing', 'completed', 'failed', 'skipped'
-- OCR Extracted Data
extracted_date DATE,
extracted_amount DECIMAL(15,2),
extracted_vendor VARCHAR(500),
extracted_description TEXT,
extracted_invoice_number VARCHAR(100),
-- Tax Information
tax_type INTEGER, -- 0=exempt, 8=reduced, 10=standard
tax_amount DECIMAL(15,2),
-- Payment Method
payment_method VARCHAR(50) DEFAULT 'personal_card',
-- Values: 'cash', 'personal_card', 'corporate_card', 'bank_transfer'
-- AI Categorization
suggested_category INTEGER NOT NULL DEFAULT 18, -- ExpenseCategory.OTHERS
category_confidence DECIMAL(3,2) DEFAULT 0.5,
alternative_categories JSONB DEFAULT '[]', -- Array of category IDs
-- Business Relevance
is_business_relevant BOOLEAN DEFAULT true,
matched_business_objective VARCHAR(500),
relevance_score DECIMAL(3,2) DEFAULT 0.5,
relevance_warning TEXT,
-- User Edits (tracks manual changes)
is_edited BOOLEAN DEFAULT false,
edited_fields JSONB DEFAULT '[]', -- Array of field names
user_override_category INTEGER, -- If user changed category
user_notes TEXT,
-- Anomaly Detection
anomalies JSONB DEFAULT '[]',
-- Array of: { type, severity, message, details, dismissed }
-- OCR Confidence
ocr_confidence DECIMAL(3,2) DEFAULT 0.0,
ocr_raw_response JSONB, -- Full OCR response for debugging
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
analyzed_at TIMESTAMPTZ,
-- Constraints
CONSTRAINT positive_amount CHECK (extracted_amount IS NULL OR extracted_amount >= 0),
CONSTRAINT valid_tax_type CHECK (tax_type IS NULL OR tax_type IN (0, 8, 10)),
CONSTRAINT valid_confidence CHECK (category_confidence >= 0 AND category_confidence <= 1)
);
-- Indexes
CREATE INDEX idx_wizard_items_session ON expense_wizard_items(session_id);
CREATE INDEX idx_wizard_items_user ON expense_wizard_items(user_id);
CREATE INDEX idx_wizard_items_status ON expense_wizard_items(analysis_status);
-- RLS Policies
ALTER TABLE expense_wizard_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage own wizard items"
ON expense_wizard_items
FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE TABLE expense_anomaly_rules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, -- NULL = system default
-- Rule Definition
rule_type VARCHAR(50) NOT NULL,
-- Values: 'high_amount', 'duplicate', 'unusual_vendor', 'suspicious_timing',
-- 'category_mismatch', 'business_irrelevant', 'missing_receipt'
rule_name VARCHAR(200) NOT NULL,
rule_description TEXT,
-- Thresholds & Configuration
config JSONB NOT NULL,
-- Examples:
-- high_amount: { threshold: 100000, severity: 'warning' }
-- duplicate: { timeWindowDays: 7, amountTolerance: 0.05 }
-- business_irrelevant: { minRelevanceScore: 0.3, severity: 'warning' }
-- Severity: 'info', 'warning', 'error'
default_severity VARCHAR(20) DEFAULT 'warning',
-- Status
is_active BOOLEAN DEFAULT true,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Unique constraint for user-specific rules
UNIQUE(user_id, rule_type)
);
-- Insert default system rules
INSERT INTO expense_anomaly_rules (user_id, rule_type, rule_name, rule_description, config, default_severity) VALUES
(NULL, 'high_amount', '高額支出アラート', '10万円以上の支出を検出', '{"threshold": 100000, "criticalThreshold": 300000}', 'warning'),
(NULL, 'duplicate', '重複検出', '同一金額・日付の取引を検出', '{"timeWindowDays": 7, "amountTolerance": 0.05}', 'warning'),
(NULL, 'business_irrelevant', '業務関連性', '定款の業務内容との関連性をチェック', '{"minRelevanceScore": 0.3}', 'warning'),
(NULL, 'suspicious_timing', 'タイミング異常', '営業時間外や休日の支出を検出', '{"businessHours": {"start": 6, "end": 22}, "includeWeekends": false}', 'info'),
(NULL, 'category_mismatch', 'カテゴリ不一致', 'AIと過去実績のカテゴリが異なる場合', '{"confidenceThreshold": 0.6}', 'info');
Create a new wizard session.
Request:
{
"tripType": "travel",
"tripInfo": {
"destination": "高原拠点",
"startDate": "2025-11-10",
"endDate": "2025-11-14",
"purpose": "リフォーム管理"
}
}
Response:
{
"success": true,
"data": {
"sessionId": "uuid-...",
"status": "draft",
"tripType": "travel",
"selectedDocTypes": ["travel", "claims", "salary_slip", "payment"],
"createdAt": "2026-01-25T14:30:00Z"
}
}
Error Responses:
400 Bad Request: Invalid trip type or dates401 Unauthorized: Not authenticated429 Too Many Requests: Rate limit exceeded (max 10 sessions/hour)Get session details with all items.
Response:
{
"success": true,
"data": {
"session": {
"id": "uuid-...",
"status": "analyzing",
"tripType": "travel",
"tripInfo": {...},
"selectedDocTypes": ["travel", "claims", "salary_slip", "payment"],
"totalAmount": 52800,
"totalItems": 12,
"dailyAllowanceTotal": 25000,
"createdAt": "2026-01-25T14:30:00Z",
"updatedAt": "2026-01-25T14:35:00Z"
},
"items": [
{
"id": "uuid-...",
"fileName": "receipt_001.pdf",
"analysisStatus": "completed",
"extractedDate": "2025-11-10",
"extractedAmount": 3000,
"extractedVendor": "NEXCO",
"suggestedCategory": 3,
"categoryConfidence": 0.92,
"isBusinessRelevant": true,
"anomalies": []
}
],
"summary": {
"pendingAnalysis": 0,
"completedAnalysis": 12,
"failedAnalysis": 0,
"totalAnomalies": 2,
"criticalAnomalies": 0
}
}
}
Update session configuration.
Request:
{
"tripType": "reimbursement",
"selectedDocTypes": ["claims", "salary_slip", "payment"],
"tripInfo": null
}
Cancel and delete a session (and all items).
Upload files for analysis.
Request: multipart/form-data
files[]: Array of files (max 20, each max 10MB)application/pdf, image/jpeg, image/png, image/webpResponse:
{
"success": true,
"data": {
"uploadedItems": [
{
"id": "uuid-...",
"fileName": "receipt_001.pdf",
"fileSize": 245678,
"analysisStatus": "pending"
}
],
"totalUploaded": 5,
"sessionItemCount": 12
}
}
Error Responses:
400 Bad Request: Invalid file type or size exceeded413 Payload Too Large: Total upload size exceeded (max 100MB)422 Unprocessable Entity: Max items per session exceeded (max 50)Trigger AI analysis for all pending items.
Request:
{
"itemIds": ["uuid-1", "uuid-2"], // Optional: specific items only
"options": {
"skipCategorization": false,
"skipAnomalyDetection": false,
"skipBusinessRelevance": false
}
}
Response:
{
"success": true,
"data": {
"jobId": "analysis-job-uuid",
"status": "processing",
"itemsQueued": 5,
"estimatedTimeSeconds": 15
}
}
Webhook/SSE for Progress:
{
"event": "analysis_progress",
"data": {
"jobId": "analysis-job-uuid",
"completed": 3,
"total": 5,
"currentItem": "uuid-...",
"status": "processing"
}
}
Poll analysis status (alternative to webhook).
Response:
{
"success": true,
"data": {
"status": "completed",
"progress": {
"total": 5,
"completed": 5,
"failed": 0,
"pending": 0
},
"items": [
{
"id": "uuid-...",
"status": "completed",
"extractedData": {...},
"anomalies": [...]
}
]
}
}
Update item data (user edits).
Request:
{
"extractedDate": "2025-12-18",
"extractedAmount": 3500,
"extractedVendor": "NEXCO Japan",
"suggestedCategory": 3,
"userNotes": "往路の高速料金",
"dismissedAnomalies": ["anomaly-uuid-1"]
}
Response:
{
"success": true,
"data": {
"item": {
"id": "uuid-...",
"isEdited": true,
"editedFields": ["extractedDate", "extractedAmount", "extractedVendor"],
"updatedAt": "2026-01-25T14:40:00Z"
}
}
}
Remove item from session.
Generate document previews before creation.
Request:
{
"includeDocTypes": ["travel", "claims", "salary_slip", "payment"]
}
Response:
{
"success": true,
"data": {
"previews": {
"travel": {
"documentName": "20251110-14_出張報告書_高原拠点_JPY52800",
"contentPreview": {
"basicInfo": {...},
"expenses": [...],
"summary": { "totalJpy": 52800, "itemCount": 13 }
}
},
"claims": {
"documentName": "20251110_経費精算書_高原拠点_JPY52800",
"contentPreview": {...}
},
"salary_slip": {...},
"payment": {...}
},
"totals": {
"expenseTotal": 32301,
"dailyAllowance": 25000,
"grandTotal": 52800,
"itemCount": 13
},
"warnings": [
{
"type": "high_amount_item",
"message": "1件の高額支出があります",
"details": "¥6,800 - 接待交際費"
}
]
}
}
Create all documents with relationships (atomic operation).
Request:
{
"idempotencyKey": "wizard-create-uuid-...",
"includeDocTypes": ["travel", "claims", "salary_slip", "payment"],
"linkToBankTransaction": true,
"bankTransactionConfig": {
"createNew": true,
"transactionDate": "2026-01-25",
"description": "経費精算 高原拠点の改修出張"
}
}
Response:
{
"success": true,
"data": {
"createdDocuments": [
{
"id": "uuid-travel-...",
"documentType": "travel",
"documentName": "20251110-14_出張報告書_高原拠点_JPY52800",
"status": "published"
},
{
"id": "uuid-claims-...",
"documentType": "claims",
"documentName": "20251110_経費精算書_高原拠点_JPY52800",
"status": "published"
},
{
"id": "uuid-salary-...",
"documentType": "salary_slip",
"documentName": "20260125_給与明細書_経費精算_JPY52800",
"status": "published"
},
{
"id": "uuid-payment-...",
"documentType": "payment",
"documentName": "20260125_支払明細書_高原拠点_JPY52800",
"status": "published"
}
],
"relationships": [
{
"parentId": "uuid-travel-...",
"childId": "uuid-claims-...",
"type": "travel_source"
},
{
"parentId": "uuid-claims-...",
"childId": "uuid-salary-...",
"type": "expense_claim_source"
},
{
"parentId": "uuid-salary-...",
"childId": "uuid-payment-...",
"type": "salary_source"
}
],
"bankTransaction": {
"id": "uuid-bank-...",
"transactionDate": 20260125,
"withdrawalAmount": 52800,
"linkedDocumentId": "uuid-payment-..."
},
"sessionStatus": "completed",
"completedAt": "2026-01-25T14:45:00Z"
}
}
Error Responses:
400 Bad Request: Invalid configuration or missing required items409 Conflict: Session already completed or idempotency key reused422 Unprocessable Entity: Unresolved critical anomalies500 Internal Server Error: Transaction failed (all changes rolled back)Validate expense against company 業務内容.
Request:
{
"description": "掃除機",
"category": 9,
"amount": 89800,
"vendor": "Amazon"
}
Response:
{
"success": true,
"data": {
"isRelevant": true,
"matchedObjective": "不動産管理",
"relevanceScore": 0.72,
"reasoning": "清掃用具は不動産管理業務に関連します",
"warning": "高額購入(¥89,800)- 固定資産計上を検討してください",
"suggestions": [
{
"type": "category_change",
"suggestedCategory": 14,
"reason": "10万円以上は備品費または固定資産として計上を推奨"
}
],
"companyObjectives": [
"メディア制作",
"IT・システム",
"不動産管理",
"キャンプ場運営",
"コンサルティング"
]
}
}
Detect anomalies in expense items.
Request:
{
"sessionId": "uuid-...",
"items": [
{
"id": "item-uuid-...",
"date": "2025-12-20",
"amount": 89800,
"vendor": "Amazon",
"category": 9,
"description": "掃除機"
}
]
}
Response:
{
"success": true,
"data": {
"anomalies": [
{
"itemId": "item-uuid-...",
"type": "high_amount",
"severity": "warning",
"message": "高額支出: ¥89,800",
"details": "平均支出の3倍以上です",
"suggestion": "固定資産として計上を検討してください",
"ruleId": "rule-uuid-..."
}
],
"summary": {
"totalAnomalies": 1,
"byType": { "high_amount": 1 },
"bySeverity": { "warning": 1, "error": 0, "info": 0 }
},
"overallRiskScore": 0.3
}
}
日当の額は、会社の出張旅費規程(書類種別 rules_claims の allowances.daily)で決まります。
| 区分 | 判定 | 規程のキー | 設定例 |
|---|---|---|---|
| 日帰り出張 | 出張期間が1日(開始日=終了日) | day_trip |
¥3,000 |
| 国内宿泊出張 | 出張期間が2日以上 | domestic_overnight |
¥5,000 |
| 海外出張 | 行き先が海外、または外貨の領収書がある | overseas |
¥8,000 |
daily_allowance_user_set: true)だけ、入力した額が規程より優先されます。DEFAULT_DAILY_ALLOWANCE_YEN)を使います。ALLOWANCE_RATE_NOT_FROM_RULES)ALLOWANCE_OVERNIGHT_WITHOUT_LODGING)ALLOWANCE_DAYS_EXCEED_RECEIPT_SPAN)ALLOWANCE_WITHOUT_RULES_DOC)例: 2025-11-10〜2025-11-14(5日間・国内宿泊出張・規程 ¥5,000)→ 5 × ¥5,000 = ¥25,000
// ExpenseCategory enum (must match frontend)
enum ExpenseCategory {
ENTERTAINMENT = 0, // 接待交際費
MEETING_EXPENSES = 1, // 会議費
OFFICE_SUPPLIES = 2, // 事務用品費
TRAVEL_TRANSPORT = 3, // 旅費交通費
ADVERTISING = 4, // 広告宣伝費
INSURANCE = 5, // 保険料
RENT = 6, // 地代家賃
COMMUNICATION = 7, // 通信費
UTILITIES = 8, // 水道光熱費
SUPPLIES = 9, // 消耗品費
OUTSOURCING = 10, // 外注費
PROFESSIONAL_FEES = 11, // 支払手数料
TAXES = 12, // 租税公課
DEPRECIATION = 13, // 減価償却費
EQUIPMENT = 14, // 備品費
FUEL = 15, // 燃料費
PRE_ESTABLISHMENT = 16, // 開業費
ESTABLISHMENT = 17, // 創立費
OTHERS = 18, // その他
ENTERTAINMENT_DINING = 19, // 接待飲食費
WELFARE_BENEFITS = 20, // 福利厚生費
BOOKS_PUBLICATIONS = 21, // 新聞図書費
MEMBERSHIP_DUES = 22, // 諸会費
}
// Relationship types for document_relationships table
const RELATIONSHIP_TYPES = {
// Travel Report -> Claims
TRAVEL_SOURCE: "travel_source",
// Claims -> Salary Slip
EXPENSE_CLAIM_SOURCE: "expense_claim_source",
// Salary Slip -> Payment
SALARY_SOURCE: "salary_source",
// Payment -> Bank Transaction (via document_transaction_links)
PAYMENT_LINK: "payment_voucher",
}
Each document type has a specific JSONB structure that must be followed exactly:
travel){
"basicInfo": {
"reporter": "代表社員 代表太郎",
"reportDate": "2026-01-25",
"travelPeriod": "2025-11-10 - 2025-11-14",
"destination": "高原拠点"
},
"purposes": {
"inspectionResearch": true,
"salesMeeting": false,
"training": false,
"other": false,
"detail": "リフォーム管理および現地確認"
},
"itinerary": [
{
"date": "2025-11-10",
"time": "",
"location": "東京 → 高原拠点",
"contact": "",
"activities": "移動"
}
],
"outcomes": {
"achievements": "リフォーム進捗確認完了",
"nextActions": "追加工事の手配"
},
"expenses": [
{
"id": 1,
"date": "2025-11-10",
"category": "交通費",
"description": "JR電車(往路)",
"foreignAmount": null,
"jpyAmount": 1990,
"runningTotal": 1990,
"paymentMethod": "個人カード",
"hasReceipt": true,
"notes": "赤羽→長野原草津口"
}
],
"summary": {
"totalJpy": 52800,
"itemCount": 13
},
"signature": {
"date": "2026-01-25",
"title": "代表社員",
"name": "代表太郎",
"seal": null
}
}
claims){
"basicInfo": {
"applicant": "代表太郎",
"position": "代表社員",
"applicationDate": "2025-11-14",
"period": "2025-11-10 - 2025-11-14"
},
"expenses": [
{
"id": "uuid-...",
"date": "2025-11-10",
"vendor": "JR東日本",
"purpose": "出張移動(往路)",
"category": 3,
"amount": 1990,
"taxType": 10,
"taxAmount": 181,
"paymentMethod": 1,
"hasReceipt": true,
"notes": "",
"attachmentIds": []
}
],
"summary": {
"totalAmount": 52800,
"expenseCount": 13
},
"signature": {
"date": "2026-01-25",
"signingMode": "none",
"inkanImage": null
}
}
salary_slip){
"employeeInfo": {
"name": "代表太郎",
"department": "",
"position": "代表社員"
},
"payPeriod": "2026-01",
"earnings": [
{
"id": "reimbursement",
"item": "立替経費精算",
"amount": 52800
}
],
"deductions": [],
"netPay": 52800,
"ytdTotals": {
"grossPay": 52800,
"totalDeductions": 0,
"netPay": 52800
}
}
payment){
"paymentInfo": {
"paymentDate": "2026-01-25",
"payee": "代表太郎",
"bankAccount": "三菱UFJ銀行 〇〇支店",
"totalAmount": 52800
},
"settlementDetails": [
{
"id": "transportation",
"item": "交通費",
"description": "往復電車・高速道路",
"amount": 10830
},
{
"id": "fuel",
"item": "燃料費",
"description": "ガソリン・灯油",
"amount": 13187
},
{
"id": "daily_allowance",
"item": "日当",
"description": "5日間×¥5,000",
"amount": 25000
}
],
"referenceInfo": {
"salarySlipDate": "2026-01-25",
"processingMethod": "経費精算",
"status": "支払済"
},
"signature": {
"date": "2026-01-25",
"title": "経理担当"
}
}
Use existing /bulk-upload/upload endpoint or integrate directly with:
Required Extractions:
interface CategorizationRequest {
description: string
amount: number
vendor: string
date: string
previousCategories?: number[] // Historical data for this vendor
}
interface CategorizationResponse {
suggestedCategory: number
confidence: number
alternatives: number[]
reasoning: string
}
AI Prompting Strategy:
You are a Japanese accounting expert categorizing business expenses.
Given:
- Description: {description}
- Amount: ¥{amount}
- Vendor: {vendor}
- Date: {date}
Categories (J-GAAP compliant):
0=接待交際費, 1=会議費, 2=事務用品費, 3=旅費交通費, ...
Consider:
1. Vendor type (e.g., NEXCO = transportation)
2. Amount (high amounts may be 備品費 or 固定資産)
3. Description keywords
4. Tax deductibility implications
Return: { category: number, confidence: 0-1, alternatives: number[], reasoning: string }
interface BusinessRelevanceRequest {
description: string
category: number
amount: number
companyObjectives: string[] // From companies.business_objectives
}
interface BusinessRelevanceResponse {
isRelevant: boolean
matchedObjective: string | null
relevanceScore: number // 0-1
reasoning: string
warning: string | null
}
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "金額は0以上である必要があります",
"details": {
"field": "extractedAmount",
"value": -100,
"constraint": "positive"
},
"requestId": "req-uuid-...",
"timestamp": "2026-01-25T14:30:00Z"
}
}
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR |
400 | Invalid request data |
NOT_FOUND |
404 | Resource not found |
UNAUTHORIZED |
401 | Not authenticated |
FORBIDDEN |
403 | Not authorized for this resource |
CONFLICT |
409 | Resource conflict (e.g., idempotency) |
RATE_LIMITED |
429 | Too many requests |
UNPROCESSABLE |
422 | Business logic error |
AI_SERVICE_ERROR |
503 | AI service unavailable |
INTERNAL_ERROR |
500 | Unexpected server error |
When AI services fail:
analysis_status = 'failed'if (aiServiceFailed) {
item.analysisStatus = "failed"
item.extractedAmount = null // User must enter manually
item.suggestedCategory = ExpenseCategory.OTHERS
item.categoryConfidence = 0
item.anomalies.push({
type: "ai_service_error",
severity: "info",
message: "AI分析が失敗しました。手動で入力してください。",
})
}
// File upload validation
const FILE_CONSTRAINTS = {
maxSizeBytes: 10 * 1024 * 1024, // 10MB
maxFilesPerUpload: 20,
maxItemsPerSession: 50,
allowedMimeTypes: [
"application/pdf",
"image/jpeg",
"image/png",
"image/webp",
],
}
// Amount validation
const AMOUNT_CONSTRAINTS = {
min: 0,
max: 999999999, // ¥999,999,999
decimalPlaces: 0, // Yen has no decimals
}
| Endpoint | Limit |
|---|---|
| POST /sessions | 10/hour |
| POST /upload | 100/hour |
| POST /analyze | 50/hour |
| POST /create | 20/hour |
| GET endpoints | 1000/hour |
| Edge Case | Handling |
|---|---|
| Corrupted PDF | Mark as failed, allow skip |
| Password-protected PDF | Return error, suggest re-upload |
| Image too blurry | Low confidence score, manual entry |
| Multi-page receipt | Extract from first page, note in UI |
| Duplicate file upload | Detect by hash, warn user |
| Foreign currency receipt | Extract both amounts, convert to JPY |
| Edge Case | Handling |
|---|---|
| Missing date | Use upload date as fallback |
| Missing amount | Require manual entry (block creation) |
| Ambiguous vendor | Provide alternatives, user selects |
| Multiple amounts on receipt | Extract total, list line items |
| Handwritten receipt | Low confidence, suggest re-scan |
| Edge Case | Handling |
|---|---|
| Session expires mid-process | Auto-save draft, resume later |
| Concurrent edits | Last-write-wins with conflict warning |
| Trip spans fiscal years | Split into two reports |
| Zero-amount items | Allow (e.g., free samples) |
| Negative amounts | Reject (refunds handled separately) |
| Very old receipts | Warning for audit risk |
| Edge Case | Handling |
|---|---|
| Partial creation failure | Rollback all, return error |
| Duplicate document names | Append timestamp |
| Missing required fields | Block creation, list missing |
| Bank account not configured | Skip bank transaction link |
# Create session
curl -X POST ローカル開発環境/api/expense-wizard/sessions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tripType": "travel", "tripInfo": {"destination": "高原拠点", "startDate": "2025-11-10", "endDate": "2025-11-14"}}'
# Upload files
curl -X POST ローカル開発環境/api/expense-wizard/sessions/$SESSION_ID/upload \
-H "Authorization: Bearer $TOKEN" \
-F "files[]=@receipt1.pdf" \
-F "files[]=@receipt2.jpg"
# Trigger analysis
curl -X POST ローカル開発環境/api/expense-wizard/sessions/$SESSION_ID/analyze \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
# Create documents
curl -X POST ローカル開発環境/api/expense-wizard/sessions/$SESSION_ID/create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{"includeDocTypes": ["travel", "claims", "salary_slip", "payment"], "linkToBankTransaction": true}'
The frontend expense wizard is implemented at:
src/features/expense-wizard/Key integration points:
useExpenseWizardStore - manages wizard stateuseExpenseWizard() - orchestrates API callsExpenseWizardModal - main UI componentThe frontend currently uses mock implementations. Replace with actual API calls:
// In src/features/expense-wizard/hooks/use-expense-wizard.ts
// Replace mockAnalyzeFile with:
const response = await fetch(
`/api/expense-wizard/sessions/${sessionId}/analyze`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ itemIds }),
}
)
// Replace createDocument with:
const response = await fetch(
`/api/expense-wizard/sessions/${sessionId}/create`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ includeDocTypes: selectedDocTypes }),
}
)
読み込み中
豆知識