First running version
This commit is contained in:
42
reference_impl/mock-services/data/customers.json
Normal file
42
reference_impl/mock-services/data/customers.json
Normal file
@@ -0,0 +1,42 @@
|
||||
[
|
||||
{
|
||||
"customerId": "C001",
|
||||
"name": "田中 花子",
|
||||
"email": "hanako.tanaka@example.com",
|
||||
"cancelTendency": "high",
|
||||
"cancelTendencyReason": "雨天キャンセル傾向",
|
||||
"reservations": ["R001", "R003"]
|
||||
},
|
||||
{
|
||||
"customerId": "C002",
|
||||
"name": "山田 太郎",
|
||||
"email": "taro.yamada@example.com",
|
||||
"cancelTendency": "high",
|
||||
"cancelTendencyReason": "雨天キャンセル傾向",
|
||||
"reservations": ["R002"]
|
||||
},
|
||||
{
|
||||
"customerId": "C003",
|
||||
"name": "鈴木 一郎",
|
||||
"email": "ichiro.suzuki@example.com",
|
||||
"cancelTendency": "high",
|
||||
"cancelTendencyReason": "雨天キャンセル傾向",
|
||||
"reservations": ["R004"]
|
||||
},
|
||||
{
|
||||
"customerId": "C004",
|
||||
"name": "佐藤 美咲",
|
||||
"email": "misaki.sato@example.com",
|
||||
"cancelTendency": "high",
|
||||
"cancelTendencyReason": "雨天キャンセル傾向",
|
||||
"reservations": ["R005"]
|
||||
},
|
||||
{
|
||||
"customerId": "C005",
|
||||
"name": "伊藤 健",
|
||||
"email": "ken.ito@example.com",
|
||||
"cancelTendency": "high",
|
||||
"cancelTendencyReason": "雨天キャンセル傾向",
|
||||
"reservations": ["R006"]
|
||||
}
|
||||
]
|
||||
37
reference_impl/mock-services/data/inventory.json
Normal file
37
reference_impl/mock-services/data/inventory.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"itemId": "DESSERT_CHOCO",
|
||||
"name": "チョコレートケーキ",
|
||||
"category": "dessert",
|
||||
"stock": 25,
|
||||
"unit": "個",
|
||||
"costPerUnit": 350
|
||||
},
|
||||
{
|
||||
"itemId": "DESSERT_PUDDING",
|
||||
"name": "プリン",
|
||||
"category": "dessert",
|
||||
"stock": 40,
|
||||
"unit": "個",
|
||||
"costPerUnit": 200
|
||||
},
|
||||
{
|
||||
"itemId": "DESSERT_TIRAMISU",
|
||||
"name": "ティラミス",
|
||||
"category": "dessert",
|
||||
"stock": 18,
|
||||
"unit": "個",
|
||||
"costPerUnit": 420
|
||||
},
|
||||
{
|
||||
"itemId": "DRINK_WINE",
|
||||
"name": "赤ワイン",
|
||||
"category": "drink",
|
||||
"stock": 12,
|
||||
"unit": "本",
|
||||
"costPerUnit": 1500
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-03-27T06:00:00Z"
|
||||
}
|
||||
62
reference_impl/mock-services/data/reservations.json
Normal file
62
reference_impl/mock-services/data/reservations.json
Normal file
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"reservationId": "R001",
|
||||
"customerId": "C001",
|
||||
"customerName": "田中 花子",
|
||||
"date": "2026-03-28",
|
||||
"time": "18:00",
|
||||
"partySize": 2,
|
||||
"status": "confirmed",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"reservationId": "R002",
|
||||
"customerId": "C002",
|
||||
"customerName": "山田 太郎",
|
||||
"date": "2026-03-28",
|
||||
"time": "19:00",
|
||||
"partySize": 4,
|
||||
"status": "confirmed",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"reservationId": "R003",
|
||||
"customerId": "C001",
|
||||
"customerName": "田中 花子",
|
||||
"date": "2026-03-28",
|
||||
"time": "19:30",
|
||||
"partySize": 2,
|
||||
"status": "confirmed",
|
||||
"notes": "アレルギー: ナッツ"
|
||||
},
|
||||
{
|
||||
"reservationId": "R004",
|
||||
"customerId": "C003",
|
||||
"customerName": "鈴木 一郎",
|
||||
"date": "2026-03-28",
|
||||
"time": "20:00",
|
||||
"partySize": 3,
|
||||
"status": "confirmed",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"reservationId": "R005",
|
||||
"customerId": "C004",
|
||||
"customerName": "佐藤 美咲",
|
||||
"date": "2026-03-28",
|
||||
"time": "18:30",
|
||||
"partySize": 2,
|
||||
"status": "confirmed",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"reservationId": "R006",
|
||||
"customerId": "C005",
|
||||
"customerName": "伊藤 健",
|
||||
"date": "2026-03-28",
|
||||
"time": "21:00",
|
||||
"partySize": 1,
|
||||
"status": "confirmed",
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
16
reference_impl/mock-services/data/weather.json
Normal file
16
reference_impl/mock-services/data/weather.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"location": "Tokyo",
|
||||
"forecasts": [
|
||||
{
|
||||
"date": "2026-03-28",
|
||||
"condition": "rainy",
|
||||
"conditionJa": "雨",
|
||||
"tempHigh": 14,
|
||||
"tempLow": 9,
|
||||
"precipitationMm": 12,
|
||||
"precipitationProbability": 90,
|
||||
"summary": "明日は一日を通して雨が降る見込みです。最高気温14度、傘の持参をお勧めします。"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-03-27T06:00:00Z"
|
||||
}
|
||||
200
reference_impl/mock-services/server.ts
Normal file
200
reference_impl/mock-services/server.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Mock Service Server — port 5100
|
||||
*
|
||||
* Simulates external/internal enterprise systems for the reference implementation.
|
||||
* MCPlet handlers call these endpoints; replacing with real services requires only
|
||||
* updating the mockServiceUrl in reference.yaml.
|
||||
*/
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const dataDir = path.join(__dirname, 'data');
|
||||
const PORT = 5100;
|
||||
|
||||
// ---- Data loaders ----
|
||||
|
||||
function loadJson<T>(filename: string): T {
|
||||
return JSON.parse(fs.readFileSync(path.join(dataDir, filename), 'utf-8')) as T;
|
||||
}
|
||||
|
||||
interface Customer {
|
||||
customerId: string;
|
||||
name: string;
|
||||
email: string;
|
||||
cancelTendency: string;
|
||||
cancelTendencyReason: string;
|
||||
reservations: string[];
|
||||
}
|
||||
|
||||
interface Reservation {
|
||||
reservationId: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
date: string;
|
||||
time: string;
|
||||
partySize: number;
|
||||
status: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
interface SentEmail {
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
sentAt: string;
|
||||
}
|
||||
|
||||
interface SentPost {
|
||||
platform: string;
|
||||
content: string;
|
||||
postedAt: string;
|
||||
}
|
||||
|
||||
// In-memory log for mock sends
|
||||
const sentEmails: SentEmail[] = [];
|
||||
const sentPosts: SentPost[] = [];
|
||||
|
||||
// ---- Route handlers ----
|
||||
|
||||
function handleCrmCustomers(url: URL, res: http.ServerResponse): void {
|
||||
const filter = url.searchParams.get('filter');
|
||||
const customers = loadJson<Customer[]>('customers.json');
|
||||
const result = filter === 'rain_cancel_tendency'
|
||||
? customers.filter((c) => c.cancelTendency === 'high')
|
||||
: customers;
|
||||
sendJson(res, 200, { customers: result, total: result.length });
|
||||
}
|
||||
|
||||
function handleCrmReservations(_url: URL, res: http.ServerResponse): void {
|
||||
// Fixed mock: return all reservations regardless of date query
|
||||
const reservations = loadJson<Reservation[]>('reservations.json');
|
||||
sendJson(res, 200, { reservations, total: reservations.length });
|
||||
}
|
||||
|
||||
function handleErpInventory(url: URL, res: http.ServerResponse): void {
|
||||
const item = url.searchParams.get('item');
|
||||
const data = loadJson<{ items: Array<{ category: string }> }>('inventory.json');
|
||||
const items = item
|
||||
? data.items.filter((i) => i.category === item || i.category.includes(item))
|
||||
: data.items;
|
||||
sendJson(res, 200, { items, total: items.length });
|
||||
}
|
||||
|
||||
function handleWeatherForecast(_url: URL, res: http.ServerResponse): void {
|
||||
// Fixed mock: always return rainy forecast regardless of date query
|
||||
const data = loadJson<{ location: string; forecasts: Array<{ date: string }> }>('weather.json');
|
||||
sendJson(res, 200, { location: data.location, forecasts: data.forecasts });
|
||||
}
|
||||
|
||||
function handleSiteStats(_url: URL, res: http.ServerResponse): void {
|
||||
sendJson(res, 200, {
|
||||
site: 'EPARK',
|
||||
period: 'last_7_days',
|
||||
pageViews: 12480,
|
||||
uniqueVisitors: 3920,
|
||||
reservationPageViews: 2150,
|
||||
conversionRate: 0.18,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleEmailSend(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const body = await parseJsonBody<{ to: string; subject: string; body: string }>(req);
|
||||
const entry: SentEmail = { ...body, sentAt: new Date().toISOString() };
|
||||
sentEmails.push(entry);
|
||||
console.log(`[mock-email] Sent to ${body.to}: ${body.subject}`);
|
||||
sendJson(res, 200, { ok: true, messageId: `mock-${Date.now()}`, sentAt: entry.sentAt });
|
||||
}
|
||||
|
||||
async function handleSnsPost(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const body = await parseJsonBody<{ platform: string; content: string }>(req);
|
||||
const entry: SentPost = { ...body, postedAt: new Date().toISOString() };
|
||||
sentPosts.push(entry);
|
||||
console.log(`[mock-sns] Posted to ${body.platform}: ${body.content.slice(0, 50)}...`);
|
||||
sendJson(res, 200, { ok: true, postId: `mock-${Date.now()}`, postedAt: entry.postedAt });
|
||||
}
|
||||
|
||||
function handleSentEmails(_url: URL, res: http.ServerResponse): void {
|
||||
sendJson(res, 200, { sentEmails, total: sentEmails.length });
|
||||
}
|
||||
|
||||
function handleSentPosts(_url: URL, res: http.ServerResponse): void {
|
||||
sendJson(res, 200, { sentPosts, total: sentPosts.length });
|
||||
}
|
||||
|
||||
// ---- Router ----
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url ?? '/', `http://localhost:${PORT}`);
|
||||
const method = req.method ?? 'GET';
|
||||
|
||||
try {
|
||||
if (method === 'GET' && url.pathname === '/crm/customers') {
|
||||
handleCrmCustomers(url, res); return;
|
||||
}
|
||||
if (method === 'GET' && url.pathname === '/crm/reservations') {
|
||||
handleCrmReservations(url, res); return;
|
||||
}
|
||||
if (method === 'GET' && url.pathname === '/erp/inventory') {
|
||||
handleErpInventory(url, res); return;
|
||||
}
|
||||
if (method === 'GET' && url.pathname === '/weather/forecast') {
|
||||
handleWeatherForecast(url, res); return;
|
||||
}
|
||||
if (method === 'GET' && url.pathname === '/site/stats') {
|
||||
handleSiteStats(url, res); return;
|
||||
}
|
||||
if (method === 'POST' && url.pathname === '/email/send') {
|
||||
void handleEmailSend(req, res); return;
|
||||
}
|
||||
if (method === 'POST' && url.pathname === '/sns/post') {
|
||||
void handleSnsPost(req, res); return;
|
||||
}
|
||||
if (method === 'GET' && url.pathname === '/debug/emails') {
|
||||
handleSentEmails(url, res); return;
|
||||
}
|
||||
if (method === 'GET' && url.pathname === '/debug/posts') {
|
||||
handleSentPosts(url, res); return;
|
||||
}
|
||||
|
||||
sendJson(res, 404, { error: `Not found: ${method} ${url.pathname}` });
|
||||
} catch (err) {
|
||||
sendJson(res, 500, { error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[mock-services] Listening on http://localhost:${PORT}`);
|
||||
console.log('[mock-services] Endpoints:');
|
||||
console.log(' GET /crm/customers?filter=rain_cancel_tendency');
|
||||
console.log(' GET /crm/reservations?date=YYYY-MM-DD');
|
||||
console.log(' GET /erp/inventory?item=dessert');
|
||||
console.log(' GET /weather/forecast?date=YYYY-MM-DD');
|
||||
console.log(' GET /site/stats');
|
||||
console.log(' POST /email/send');
|
||||
console.log(' POST /sns/post');
|
||||
console.log(' GET /debug/emails (sent email log)');
|
||||
console.log(' GET /debug/posts (sent SNS post log)');
|
||||
});
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
function sendJson(res: http.ServerResponse, status: number, body: unknown): void {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(body, null, 2));
|
||||
}
|
||||
|
||||
function parseJsonBody<T>(req: http.IncomingMessage): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
req.on('data', (chunk: Buffer) => { data += chunk.toString(); });
|
||||
req.on('end', () => {
|
||||
try { resolve(JSON.parse(data) as T); }
|
||||
catch { reject(new Error('Invalid JSON')); }
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user