Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Changelog

## [0.3.0] - 2026-03-16

### Added
- MCP Server expanded: 13 → 53 tools (list + create + action + report for all modules)
- 63 business flow tests verified (test_all_flows.sh)

### Fixed
- Invoice void: now accepts posted status + payment guard prevents voiding paid invoices
- CLI: auth login piped stdin TTY detection
- CLI: invoice/payment/bill response shape (items vs data)
- CLI: tax/roles/webhooks flat array handling
- CLI: settings response unwrapping
- CLI: AR paths /ar/invoices → /invoices
- All 34 CLI commands verified passing

## [0.2.0] - 2026-03-16

### Added
- MCP Server (`apps/mcp`) — 13 tools for AI integration via Model Context Protocol
- Architecture diagram + Test plan document (346 test cases)

### Fixed
- Web UI data binding: null safety (BigInt ?? 0), status mapping (void→voided)
- 16 Web UI pages fixed
- Auth hydration: protected layout waits for zustand rehydrate
- Rate limiter: 10,000 req/min in dev mode

### Changed
- Sidebar redesigned: SAP-style grouped menus (bilingual Thai+EN)
- README rewritten: explains EIP vs ERP, pain points, AI-Native approach
- API descriptions: 186/186 endpoints documented in Swagger

## [0.1.0] - 2026-03-15

### Added
- Initial release: 31 ERP modules
- 186 API endpoints (Fastify 5.8)
- 81 Web UI pages (Next.js 15)
- 39 CLI commands (Commander.js)
- 58 DB tables (PostgreSQL 17, RLS multi-tenant)
- Thai compliance: VAT 7%, WHT, SSC, PDPA, TFAC
- Audit trail: auto-log all mutations
- 417 unit tests passing
2 changes: 1 addition & 1 deletion apps/api/src/routes/ar/invoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ export async function invoiceRoutes(
const rows = await fastify.sql<[InvoiceRow?]>`
UPDATE invoices
SET status = 'void', updated_at = NOW()
WHERE id = ${id} AND tenant_id = ${tenantId} AND status IN ('draft', 'sent')
WHERE id = ${id} AND tenant_id = ${tenantId} AND status IN ('draft', 'sent', 'posted')
RETURNING *
`;
const inv = rows[0];
Expand Down
23 changes: 14 additions & 9 deletions apps/cli/src/commands/ap/bill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ interface Bill {
createdAt: string;
}

/** Paginated list response wrapper. */
/** Paginated list response wrapper (API returns items/total/limit/offset). */
interface PaginatedResponse<T> {
data: T[];
items: T[];
total: number;
page: number;
pageSize: number;
limit: number;
offset: number;
hasMore?: boolean;
}

/** Options accepted by `ap bill list`. */
Expand Down Expand Up @@ -100,9 +101,11 @@ function daysFromToday(days: number): string {
// ---------------------------------------------------------------------------

async function billList(options: BillListOptions): Promise<void> {
const pageNum = parseInt(options.page, 10);
const pageSizeNum = parseInt(options.pageSize, 10);
const params: Record<string, string> = {
page: options.page,
pageSize: options.pageSize,
limit: options.pageSize,
offset: String((pageNum - 1) * pageSizeNum),
};

if (options.status !== undefined && options.status !== '') {
Expand All @@ -119,11 +122,13 @@ async function billList(options: BillListOptions): Promise<void> {
process.exit(1);
}

const { data, total, page, pageSize } = result.data;
const { items, total, limit, offset } = result.data;
const page = Math.floor(offset / limit) + 1;
const totalPages = Math.ceil(total / limit) || 1;

printSuccess(
data,
`Showing ${String(data.length)} of ${String(total)} bills (page ${String(page)}/${String(Math.ceil(total / pageSize))})`,
items,
`Showing ${String(items.length)} of ${String(total)} bills (page ${String(page)}/${String(totalPages)})`,
);
}

Expand Down
23 changes: 14 additions & 9 deletions apps/cli/src/commands/ap/payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ interface BillPayment {
createdAt: string;
}

/** Paginated list response wrapper. */
/** Paginated list response wrapper (API returns items/total/limit/offset). */
interface PaginatedResponse<T> {
data: T[];
items: T[];
total: number;
page: number;
pageSize: number;
limit: number;
offset: number;
hasMore?: boolean;
}

/** Options accepted by `ap payment list`. */
Expand Down Expand Up @@ -103,9 +104,11 @@ function today(): string {
// ---------------------------------------------------------------------------

async function apPaymentList(options: ApPaymentListOptions): Promise<void> {
const pageNum = parseInt(options.page, 10);
const pageSizeNum = parseInt(options.pageSize, 10);
const params: Record<string, string> = {
page: options.page,
pageSize: options.pageSize,
limit: options.pageSize,
offset: String((pageNum - 1) * pageSizeNum),
};

if (options.vendorId !== undefined && options.vendorId !== '') {
Expand All @@ -122,11 +125,13 @@ async function apPaymentList(options: ApPaymentListOptions): Promise<void> {
process.exit(1);
}

const { data, total, page, pageSize } = result.data;
const { items, total, limit, offset } = result.data;
const page = Math.floor(offset / limit) + 1;
const totalPages = Math.ceil(total / limit) || 1;

printSuccess(
data,
`Showing ${String(data.length)} of ${String(total)} payments (page ${String(page)}/${String(Math.ceil(total / pageSize))})`,
items,
`Showing ${String(items.length)} of ${String(total)} payments (page ${String(page)}/${String(totalPages)})`,
);
}

Expand Down
29 changes: 17 additions & 12 deletions apps/cli/src/commands/ar/invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ interface Invoice {
createdAt: string;
}

/** Paginated list response wrapper. */
/** Paginated list response wrapper (API returns items/total/limit/offset). */
interface PaginatedResponse<T> {
data: T[];
items: T[];
total: number;
page: number;
pageSize: number;
limit: number;
offset: number;
hasMore?: boolean;
}

/** Options accepted by `ar invoice list`. */
Expand Down Expand Up @@ -160,7 +161,7 @@ async function invoiceCreate(): Promise<void> {
};

const result = await api.post<{ data: Invoice }>(
'/api/v1/ar/invoices',
'/api/v1/invoices',
payload,
);

Expand All @@ -173,9 +174,11 @@ async function invoiceCreate(): Promise<void> {
}

async function invoiceList(options: InvoiceListOptions): Promise<void> {
const pageNum = parseInt(options.page, 10);
const pageSizeNum = parseInt(options.pageSize, 10);
const params: Record<string, string> = {
page: options.page,
pageSize: options.pageSize,
limit: options.pageSize,
offset: String((pageNum - 1) * pageSizeNum),
};

if (options.status !== undefined && options.status !== '') {
Expand All @@ -186,7 +189,7 @@ async function invoiceList(options: InvoiceListOptions): Promise<void> {
}

const result = await api.get<PaginatedResponse<Invoice>>(
'/api/v1/ar/invoices',
'/api/v1/invoices',
params,
);

Expand All @@ -195,11 +198,13 @@ async function invoiceList(options: InvoiceListOptions): Promise<void> {
process.exit(1);
}

const { data, total, page, pageSize } = result.data;
const { items, total, limit, offset } = result.data;
const page = Math.floor(offset / limit) + 1;
const totalPages = Math.ceil(total / limit) || 1;

printSuccess(
data,
`Showing ${String(data.length)} of ${String(total)} invoices (page ${String(page)}/${String(Math.ceil(total / pageSize))})`,
items,
`Showing ${String(items.length)} of ${String(total)} invoices (page ${String(page)}/${String(totalPages)})`,
);
}

Expand All @@ -210,7 +215,7 @@ async function invoiceVoid(id: string): Promise<void> {
}

const result = await api.post<{ data: Invoice }>(
`/api/v1/ar/invoices/${id}/void`,
`/api/v1/invoices/${id}/void`,
);

if (!result.ok) {
Expand Down
27 changes: 16 additions & 11 deletions apps/cli/src/commands/ar/payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ interface Payment {
createdAt: string;
}

/** Paginated list response wrapper. */
/** Paginated list response wrapper (API returns items/total/limit/offset). */
interface PaginatedResponse<T> {
data: T[];
items: T[];
total: number;
page: number;
pageSize: number;
limit: number;
offset: number;
hasMore?: boolean;
}

/** Options accepted by `ar payment list`. */
Expand Down Expand Up @@ -172,7 +173,7 @@ async function paymentCreate(): Promise<void> {
};

const result = await api.post<{ data: Payment }>(
'/api/v1/ar/payments',
'/api/v1/payments',
payload,
);

Expand All @@ -185,9 +186,11 @@ async function paymentCreate(): Promise<void> {
}

async function paymentList(options: PaymentListOptions): Promise<void> {
const pageNum = parseInt(options.page, 10);
const pageSizeNum = parseInt(options.pageSize, 10);
const params: Record<string, string> = {
page: options.page,
pageSize: options.pageSize,
limit: options.pageSize,
offset: String((pageNum - 1) * pageSizeNum),
};

if (options.customerId !== undefined && options.customerId !== '') {
Expand All @@ -198,7 +201,7 @@ async function paymentList(options: PaymentListOptions): Promise<void> {
}

const result = await api.get<PaginatedResponse<Payment>>(
'/api/v1/ar/payments',
'/api/v1/payments',
params,
);

Expand All @@ -207,11 +210,13 @@ async function paymentList(options: PaymentListOptions): Promise<void> {
process.exit(1);
}

const { data, total, page, pageSize } = result.data;
const { items, total, limit, offset } = result.data;
const page = Math.floor(offset / limit) + 1;
const totalPages = Math.ceil(total / limit) || 1;

printSuccess(
data,
`Showing ${String(data.length)} of ${String(total)} payments (page ${String(page)}/${String(Math.ceil(total / pageSize))})`,
items,
`Showing ${String(items.length)} of ${String(total)} payments (page ${String(page)}/${String(totalPages)})`,
);
}

Expand Down
64 changes: 38 additions & 26 deletions apps/cli/src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,42 +41,54 @@ interface JwtClaims {
/**
* Prompt the user for a value on stdin and return what they typed.
* When `hidden` is true the input is masked (used for passwords).
* Falls back gracefully to plain readline when stdin is not a TTY
* (e.g. when input is piped via `printf "email\npass\n" | neip auth login`).
*/
async function prompt(question: string, hidden = false): Promise<string> {
return new Promise((resolve) => {
const isTTY = process.stdin.isTTY === true;

// If not a TTY (piped) or hidden mode not needed, use plain readline
if (!hidden || !isTTY) {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
process.stdout.write(question);
rl.once('line', (answer) => {
rl.close();
resolve(answer.trim());
});
return;
}

// TTY hidden mode — suppress echoing with setRawMode
const rl = createInterface({
input: process.stdin,
output: process.stdout,
terminal: true,
});

if (hidden) {
// Write the prompt manually so we can suppress echoing
process.stdout.write(question);
process.stdin.setRawMode(true);

let input = '';
process.stdin.on('data', function onData(buf: Buffer) {
const char = buf.toString();
if (char === '\n' || char === '\r' || char === '\u0003') {
process.stdin.setRawMode(false);
process.stdin.removeListener('data', onData);
process.stdout.write('\n');
rl.close();
resolve(input);
} else if (char === '\u007f') {
// Backspace
input = input.slice(0, -1);
} else {
input += char;
}
});
} else {
rl.question(question, (answer) => {
process.stdout.write(question);
process.stdin.setRawMode(true);

let input = '';
process.stdin.on('data', function onData(buf: Buffer) {
const char = buf.toString();
if (char === '\n' || char === '\r' || char === '\u0003') {
process.stdin.setRawMode(false);
process.stdin.removeListener('data', onData);
process.stdout.write('\n');
rl.close();
resolve(answer);
});
}
resolve(input);
} else if (char === '\u007f') {
// Backspace
input = input.slice(0, -1);
} else {
input += char;
}
});
});
}

Expand Down
Loading
Loading