feat: add yuntu report filling automation
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
PORT=3000
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/yuntu_report
|
||||
ALLOWED_ORIGIN=https://yuntu.oceanengine.com
|
||||
@@ -0,0 +1,18 @@
|
||||
# Yuntu Report Local Server
|
||||
|
||||
## Setup
|
||||
|
||||
1. Copy `.env.example` to `.env`.
|
||||
2. Update `DATABASE_URL` to point at your local PostgreSQL instance.
|
||||
3. Optionally adjust `ALLOWED_ORIGIN` if you need to call the server from another origin.
|
||||
4. Install dependencies with `npm install`.
|
||||
5. Start the server with `npm run dev` or `npm start`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /health`
|
||||
- `POST /api/reports`
|
||||
|
||||
## Default URL
|
||||
|
||||
- `http://localhost:3000`
|
||||
+1014
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "yuntu-report-filling-server",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Local backend for persisting Yuntu report creation records",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"dev": "node --watch src/index.js",
|
||||
"start": "node src/index.js",
|
||||
"test": "node --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.2",
|
||||
"pg": "^8.13.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
DROP TABLE IF EXISTS public.yuntu_report_info;
|
||||
|
||||
CREATE TABLE public.yuntu_report_info (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
source_type VARCHAR(32) NOT NULL,
|
||||
source_report_id VARCHAR(64) NULL,
|
||||
aadvid VARCHAR(32) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
price JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
rules JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
analysis_dims JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
categories JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
transaction_channels JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
start_time DATE NOT NULL,
|
||||
end_time DATE NOT NULL,
|
||||
period_type VARCHAR(32) NULL,
|
||||
user_name TEXT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_yuntu_report_info_report_id UNIQUE (report_id),
|
||||
CONSTRAINT chk_yuntu_report_info_source_type
|
||||
CHECK (source_type IN ('MANUAL_CAPTURE', 'AUTO_COPY')),
|
||||
CONSTRAINT chk_yuntu_report_info_copy_source
|
||||
CHECK (
|
||||
(source_type = 'MANUAL_CAPTURE' AND source_report_id IS NULL)
|
||||
OR
|
||||
(source_type = 'AUTO_COPY' AND source_report_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_yuntu_report_info_source_report_id
|
||||
ON public.yuntu_report_info (source_report_id);
|
||||
|
||||
CREATE INDEX idx_yuntu_report_info_created_at
|
||||
ON public.yuntu_report_info (created_at DESC);
|
||||
|
||||
CREATE INDEX idx_yuntu_report_info_aadvid_created_at
|
||||
ON public.yuntu_report_info (aadvid, created_at DESC);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.set_yuntu_report_info_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_yuntu_report_info_updated_at ON public.yuntu_report_info;
|
||||
|
||||
CREATE TRIGGER trg_yuntu_report_info_updated_at
|
||||
BEFORE UPDATE ON public.yuntu_report_info
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.set_yuntu_report_info_updated_at();
|
||||
@@ -0,0 +1,15 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
function createPool(env = process.env) {
|
||||
if (!env.DATABASE_URL) {
|
||||
throw new Error('DATABASE_URL is required');
|
||||
}
|
||||
|
||||
return new Pool({
|
||||
connectionString: env.DATABASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createPool,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
const { createPool } = require('./db');
|
||||
const { createRepository } = require('./report-repository');
|
||||
const { createApp } = require('./server');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || '3000', 10);
|
||||
const allowedOrigin = process.env.ALLOWED_ORIGIN || 'https://yuntu.oceanengine.com';
|
||||
|
||||
async function start() {
|
||||
const pool = createPool(process.env);
|
||||
const repository = createRepository(pool);
|
||||
const app = createApp({ repository, allowedOrigin });
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Yuntu report server listening on http://localhost:${port}`);
|
||||
});
|
||||
|
||||
const shutdown = async () => {
|
||||
server.close(async () => {
|
||||
await pool.end();
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
}
|
||||
|
||||
start().catch((error) => {
|
||||
console.error('Failed to start Yuntu report server');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
const INSERT_REPORT_SQL = `
|
||||
INSERT INTO public.yuntu_report_info (
|
||||
report_id,
|
||||
source_type,
|
||||
source_report_id,
|
||||
aadvid,
|
||||
name,
|
||||
price,
|
||||
rules,
|
||||
analysis_dims,
|
||||
categories,
|
||||
transaction_channels,
|
||||
start_time,
|
||||
end_time,
|
||||
period_type,
|
||||
user_name,
|
||||
payload
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6::jsonb, $7::jsonb, $8::jsonb, $9::jsonb, $10::jsonb,
|
||||
$11, $12, $13, $14, $15::jsonb
|
||||
)
|
||||
ON CONFLICT (report_id) DO NOTHING
|
||||
RETURNING id, report_id
|
||||
`;
|
||||
|
||||
const FIND_REPORT_SQL = `
|
||||
SELECT id, report_id
|
||||
FROM public.yuntu_report_info
|
||||
WHERE report_id = $1
|
||||
`;
|
||||
|
||||
function createRepository(pool) {
|
||||
return {
|
||||
async save(record) {
|
||||
const insertValues = [
|
||||
record.report_id,
|
||||
record.source_type,
|
||||
record.source_report_id,
|
||||
record.aadvid,
|
||||
record.name,
|
||||
JSON.stringify(record.price),
|
||||
JSON.stringify(record.rules),
|
||||
JSON.stringify(record.analysis_dims),
|
||||
JSON.stringify(record.categories),
|
||||
JSON.stringify(record.transaction_channels),
|
||||
record.start_time,
|
||||
record.end_time,
|
||||
record.period_type,
|
||||
record.user_name,
|
||||
JSON.stringify(record.payload),
|
||||
];
|
||||
|
||||
const insertResult = await pool.query(INSERT_REPORT_SQL, insertValues);
|
||||
|
||||
if (insertResult.rows.length > 0) {
|
||||
const row = insertResult.rows[0];
|
||||
return {
|
||||
id: row.id,
|
||||
reportId: row.report_id,
|
||||
created: true,
|
||||
};
|
||||
}
|
||||
|
||||
const existingResult = await pool.query(FIND_REPORT_SQL, [record.report_id]);
|
||||
const existingRow = existingResult.rows[0];
|
||||
|
||||
if (!existingRow) {
|
||||
throw new Error(`report_id ${record.report_id} was not found after conflict`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: existingRow.id,
|
||||
reportId: existingRow.report_id,
|
||||
created: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRepository,
|
||||
FIND_REPORT_SQL,
|
||||
INSERT_REPORT_SQL,
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
const ALLOWED_SOURCE_TYPES = new Set(['MANUAL_CAPTURE', 'AUTO_COPY']);
|
||||
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
class ValidationError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
this.code = 'VALIDATION_ERROR';
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNonEmptyString(value, fieldName) {
|
||||
if (typeof value !== 'string' || value.trim() === '') {
|
||||
throw new ValidationError(`${fieldName} is required`);
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function ensureOptionalString(value, fieldName) {
|
||||
if (value == null || value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ensureNonEmptyString(value, fieldName);
|
||||
}
|
||||
|
||||
function ensureArray(value, fieldName) {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new ValidationError(`${fieldName} must be an array`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function ensureObject(value, fieldName) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new ValidationError(`${fieldName} must be an object`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function ensureDateString(value, fieldName) {
|
||||
const normalized = ensureNonEmptyString(value, fieldName);
|
||||
|
||||
if (!DATE_PATTERN.test(normalized)) {
|
||||
throw new ValidationError(`${fieldName} must match YYYY-MM-DD`);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function validateAndNormalizeReportInput(input) {
|
||||
const payload = ensureObject(input, 'body');
|
||||
const sourceType = ensureNonEmptyString(payload.sourceType, 'sourceType');
|
||||
|
||||
if (!ALLOWED_SOURCE_TYPES.has(sourceType)) {
|
||||
throw new ValidationError('sourceType must be MANUAL_CAPTURE or AUTO_COPY');
|
||||
}
|
||||
|
||||
const sourceReportId = ensureOptionalString(payload.sourceReportId, 'sourceReportId');
|
||||
|
||||
if (sourceType === 'MANUAL_CAPTURE' && sourceReportId !== null) {
|
||||
throw new ValidationError('sourceReportId must be empty for MANUAL_CAPTURE');
|
||||
}
|
||||
|
||||
if (sourceType === 'AUTO_COPY' && sourceReportId === null) {
|
||||
throw new ValidationError('sourceReportId is required when sourceType is AUTO_COPY');
|
||||
}
|
||||
|
||||
return {
|
||||
reportId: ensureNonEmptyString(payload.reportId, 'reportId'),
|
||||
sourceType,
|
||||
sourceReportId,
|
||||
aadvid: ensureNonEmptyString(payload.aadvid, 'aadvid'),
|
||||
name: ensureNonEmptyString(payload.name, 'name'),
|
||||
price: ensureArray(payload.price, 'price'),
|
||||
rules: ensureArray(payload.rules, 'rules'),
|
||||
analysisDims: ensureArray(payload.analysisDims, 'analysisDims'),
|
||||
categories: ensureArray(payload.categories, 'categories'),
|
||||
channels: ensureArray(payload.channels, 'channels'),
|
||||
startTime: ensureDateString(payload.startTime, 'startTime'),
|
||||
endTime: ensureDateString(payload.endTime, 'endTime'),
|
||||
periodType: ensureOptionalString(payload.periodType, 'periodType'),
|
||||
userName: ensureOptionalString(payload.userName, 'userName'),
|
||||
payload: ensureObject(payload.payload, 'payload'),
|
||||
};
|
||||
}
|
||||
|
||||
function toDatabaseRecord(normalizedReport) {
|
||||
return {
|
||||
report_id: normalizedReport.reportId,
|
||||
source_type: normalizedReport.sourceType,
|
||||
source_report_id: normalizedReport.sourceReportId,
|
||||
aadvid: normalizedReport.aadvid,
|
||||
name: normalizedReport.name,
|
||||
price: normalizedReport.price,
|
||||
rules: normalizedReport.rules,
|
||||
analysis_dims: normalizedReport.analysisDims,
|
||||
categories: normalizedReport.categories,
|
||||
transaction_channels: normalizedReport.channels,
|
||||
start_time: normalizedReport.startTime,
|
||||
end_time: normalizedReport.endTime,
|
||||
period_type: normalizedReport.periodType,
|
||||
user_name: normalizedReport.userName,
|
||||
payload: normalizedReport.payload,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ValidationError,
|
||||
validateAndNormalizeReportInput,
|
||||
toDatabaseRecord,
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
const cors = require('cors');
|
||||
const express = require('express');
|
||||
|
||||
const {
|
||||
ValidationError,
|
||||
toDatabaseRecord,
|
||||
validateAndNormalizeReportInput,
|
||||
} = require('./report-service');
|
||||
|
||||
function createApp({ repository, allowedOrigin }) {
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: allowedOrigin,
|
||||
}),
|
||||
);
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
app.get('/health', (_request, response) => {
|
||||
response.json({
|
||||
success: true,
|
||||
data: {
|
||||
status: 'ok',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/reports', async (request, response) => {
|
||||
try {
|
||||
const normalizedReport = validateAndNormalizeReportInput(request.body);
|
||||
const record = toDatabaseRecord(normalizedReport);
|
||||
const saved = await repository.save(record);
|
||||
|
||||
response.status(saved.created ? 201 : 200).json({
|
||||
success: true,
|
||||
data: {
|
||||
id: saved.id,
|
||||
reportId: saved.reportId,
|
||||
created: saved.created,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ValidationError) {
|
||||
response.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
response.status(500).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: error.message || 'Unexpected server error',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createApp,
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
validateAndNormalizeReportInput,
|
||||
toDatabaseRecord,
|
||||
} = require('../src/report-service');
|
||||
|
||||
function createManualPayload() {
|
||||
return {
|
||||
reportId: 'report-001',
|
||||
sourceType: 'MANUAL_CAPTURE',
|
||||
sourceReportId: null,
|
||||
aadvid: '1648829117232140',
|
||||
name: '测试报告',
|
||||
price: ['1,100', '101,100000'],
|
||||
rules: [{ keywords: ['奶粉'], op: 'INCLUDE' }],
|
||||
analysisDims: ['MARKETOVERVIEW'],
|
||||
categories: [{ id: '20028', name: '奶粉类目' }],
|
||||
channels: ['ALL'],
|
||||
startTime: '2025-03-01',
|
||||
endTime: '2026-02-28',
|
||||
periodType: 'MONTH',
|
||||
userName: 'tester@example.com',
|
||||
payload: {
|
||||
name: '测试报告',
|
||||
startTime: '2025-03-01',
|
||||
endTime: '2026-02-28',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('validateAndNormalizeReportInput accepts a valid manual report payload', () => {
|
||||
const input = createManualPayload();
|
||||
|
||||
const result = validateAndNormalizeReportInput(input);
|
||||
|
||||
assert.equal(result.reportId, 'report-001');
|
||||
assert.equal(result.sourceType, 'MANUAL_CAPTURE');
|
||||
assert.equal(result.sourceReportId, null);
|
||||
assert.equal(result.aadvid, '1648829117232140');
|
||||
assert.equal(result.startTime, '2025-03-01');
|
||||
assert.equal(result.endTime, '2026-02-28');
|
||||
});
|
||||
|
||||
test('validateAndNormalizeReportInput rejects AUTO_COPY without sourceReportId', () => {
|
||||
const input = {
|
||||
...createManualPayload(),
|
||||
reportId: 'report-002',
|
||||
sourceType: 'AUTO_COPY',
|
||||
sourceReportId: '',
|
||||
};
|
||||
|
||||
assert.throws(
|
||||
() => validateAndNormalizeReportInput(input),
|
||||
(error) => {
|
||||
assert.equal(error.code, 'VALIDATION_ERROR');
|
||||
assert.match(error.message, /sourceReportId/i);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('toDatabaseRecord maps API payload fields into database-ready values', () => {
|
||||
const normalized = validateAndNormalizeReportInput({
|
||||
...createManualPayload(),
|
||||
reportId: 'report-003',
|
||||
sourceType: 'AUTO_COPY',
|
||||
sourceReportId: 'report-001',
|
||||
});
|
||||
|
||||
const record = toDatabaseRecord(normalized);
|
||||
|
||||
assert.deepEqual(record, {
|
||||
report_id: 'report-003',
|
||||
source_type: 'AUTO_COPY',
|
||||
source_report_id: 'report-001',
|
||||
aadvid: '1648829117232140',
|
||||
name: '测试报告',
|
||||
price: ['1,100', '101,100000'],
|
||||
rules: [{ keywords: ['奶粉'], op: 'INCLUDE' }],
|
||||
analysis_dims: ['MARKETOVERVIEW'],
|
||||
categories: [{ id: '20028', name: '奶粉类目' }],
|
||||
transaction_channels: ['ALL'],
|
||||
start_time: '2025-03-01',
|
||||
end_time: '2026-02-28',
|
||||
period_type: 'MONTH',
|
||||
user_name: 'tester@example.com',
|
||||
payload: {
|
||||
name: '测试报告',
|
||||
startTime: '2025-03-01',
|
||||
endTime: '2026-02-28',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { createApp } = require('../src/server');
|
||||
|
||||
function createManualPayload() {
|
||||
return {
|
||||
reportId: 'report-001',
|
||||
sourceType: 'MANUAL_CAPTURE',
|
||||
sourceReportId: null,
|
||||
aadvid: '1648829117232140',
|
||||
name: '测试报告',
|
||||
price: ['1,100', '101,100000'],
|
||||
rules: [{ keywords: ['奶粉'], op: 'INCLUDE' }],
|
||||
analysisDims: ['MARKETOVERVIEW'],
|
||||
categories: [{ id: '20028', name: '奶粉类目' }],
|
||||
channels: ['ALL'],
|
||||
startTime: '2025-03-01',
|
||||
endTime: '2026-02-28',
|
||||
periodType: 'MONTH',
|
||||
userName: 'tester@example.com',
|
||||
payload: {
|
||||
name: '测试报告',
|
||||
startTime: '2025-03-01',
|
||||
endTime: '2026-02-28',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function withServer(repository, callback) {
|
||||
const app = createApp({
|
||||
repository,
|
||||
allowedOrigin: 'https://yuntu.oceanengine.com',
|
||||
});
|
||||
|
||||
const server = await new Promise((resolve) => {
|
||||
const instance = app.listen(0, '127.0.0.1', () => resolve(instance));
|
||||
});
|
||||
|
||||
const address = server.address();
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
|
||||
try {
|
||||
await callback(baseUrl);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test('GET /health returns ok status', async () => {
|
||||
await withServer(
|
||||
{
|
||||
async save() {
|
||||
throw new Error('save should not be called');
|
||||
},
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const response = await fetch(`${baseUrl}/health`);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(body, {
|
||||
success: true,
|
||||
data: {
|
||||
status: 'ok',
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('POST /api/reports returns 400 for invalid auto copy payload', async () => {
|
||||
await withServer(
|
||||
{
|
||||
async save() {
|
||||
throw new Error('save should not be called');
|
||||
},
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const response = await fetch(`${baseUrl}/api/reports`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...createManualPayload(),
|
||||
sourceType: 'AUTO_COPY',
|
||||
sourceReportId: '',
|
||||
}),
|
||||
});
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(body.success, false);
|
||||
assert.equal(body.error.code, 'VALIDATION_ERROR');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('POST /api/reports persists a valid payload and returns created response', async () => {
|
||||
let savedRecord = null;
|
||||
|
||||
await withServer(
|
||||
{
|
||||
async save(record) {
|
||||
savedRecord = record;
|
||||
return {
|
||||
id: 12,
|
||||
reportId: record.report_id,
|
||||
created: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const response = await fetch(`${baseUrl}/api/reports`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(createManualPayload()),
|
||||
});
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 201);
|
||||
assert.deepEqual(body, {
|
||||
success: true,
|
||||
data: {
|
||||
id: 12,
|
||||
reportId: 'report-001',
|
||||
created: true,
|
||||
},
|
||||
});
|
||||
assert.equal(savedRecord.report_id, 'report-001');
|
||||
assert.equal(savedRecord.source_type, 'MANUAL_CAPTURE');
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user