Skip to content

Commit 1d2bc36

Browse files
Merge pull request #275 from Olamidepy/main
#215 Implement OECD CARF/DAC8 Annual Tax Reporting Engine
2 parents 83555f7 + 9133974 commit 1d2bc36

8 files changed

Lines changed: 3434 additions & 569 deletions

File tree

‎index.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,9 @@ async function createApp(dependencies = {}) {
404404
// Sandbox API
405405
app.use('/api/sandbox', require('./routes/sandbox'));
406406

407+
// Tax reporting API
408+
app.use('/tax', require('./routes/tax'));
409+
407410
// Social token gating endpoints
408411
app.use('/api/social-token', createSocialTokenRoutes());
409412

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
exports.up = async function up(knex) {
2+
await knex.schema.createTable('tax_reportable_transactions', (table) => {
3+
table.string('id').primary();
4+
table.string('user_address').notNullable();
5+
table.string('merchant_id').nullable();
6+
table.string('asset_code').notNullable();
7+
table.decimal('amount', 36, 12).notNullable();
8+
table.string('transaction_kind').notNullable();
9+
table.timestamp('occurred_at').notNullable();
10+
table.text('metadata_json').nullable();
11+
12+
table.index(['occurred_at']);
13+
table.index(['user_address']);
14+
table.index(['merchant_id']);
15+
});
16+
17+
await knex.schema.createTable('tax_report_audit_log', (table) => {
18+
table.string('id').primary();
19+
table.string('report_id').notNullable();
20+
table.integer('version').notNullable();
21+
table.string('status').notNullable();
22+
table.string('schema_version').notNullable();
23+
table.integer('reporting_year').notNullable();
24+
table.string('jurisdiction').notNullable();
25+
table.string('primary_currency').notNullable();
26+
table.string('payload_hash').notNullable();
27+
table.string('previous_hash').nullable();
28+
table.text('payload_json').notNullable();
29+
table.text('payload_xml').notNullable();
30+
table.string('generated_by').notNullable();
31+
table.string('signed_off_by').nullable();
32+
table.timestamp('signed_off_at').nullable();
33+
table.timestamp('retention_until').notNullable();
34+
table.timestamp('created_at').notNullable();
35+
36+
table.unique(['report_id', 'version']);
37+
table.index(['report_id', 'version']);
38+
table.index(['reporting_year', 'jurisdiction']);
39+
});
40+
41+
await knex.schema.createTable('tax_report_signoffs', (table) => {
42+
table.string('id').primary();
43+
table.string('report_id').notNullable();
44+
table.integer('version').notNullable();
45+
table.string('signed_off_by').notNullable();
46+
table.timestamp('signed_off_at').notNullable();
47+
table.text('notes').nullable();
48+
table.string('payload_hash').notNullable();
49+
50+
table.index(['report_id', 'version']);
51+
});
52+
};
53+
54+
exports.down = async function down(knex) {
55+
await knex.schema.dropTableIfExists('tax_report_signoffs');
56+
await knex.schema.dropTableIfExists('tax_report_audit_log');
57+
await knex.schema.dropTableIfExists('tax_reportable_transactions');
58+
};

‎package-lock.json‎

Lines changed: 2636 additions & 179 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,11 @@
105105
"web3.storage": "^4.5.4",
106106
"winston": "^3.19.0",
107107
"xlsx": "^0.18.5",
108-
"@opentelemetry/api": "^1.18.0",
109-
"@opentelemetry/sdk-node": "^0.66.0",
110-
"@opentelemetry/auto-instrumentations-node": "^0.66.0",
111-
"@opentelemetry/exporter-trace-otlp-http": "^0.66.0",
112-
"@opentelemetry/sdk-trace-base": "^0.66.0"
108+
"@opentelemetry/api": "^1.9.1",
109+
"@opentelemetry/sdk-node": "^0.215.0",
110+
"@opentelemetry/auto-instrumentations-node": "^0.73.0",
111+
"@opentelemetry/exporter-trace-otlp-http": "^0.215.0",
112+
"@opentelemetry/sdk-trace-base": "^2.7.0"
113113
},
114114
"devDependencies": {
115115
"@nestjs/cli": "^11.0.21",

‎routes/tax.js‎

Lines changed: 116 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,155 +2,183 @@ const express = require('express');
22
const router = express.Router();
33
const taxService = require('../services/taxService');
44

5-
// Generate tax report for a creator
6-
router.get('/report/:creatorAddress/:year', async (req, res) => {
5+
function getService(req) {
6+
const database = req.app?.get?.('database') || req.database || null;
7+
return database ? taxService.withDatabase(database) : taxService;
8+
}
9+
10+
function parseYear(year) {
11+
const parsed = Number(year);
12+
return Number.isInteger(parsed) ? parsed : null;
13+
}
14+
15+
router.post('/carf-dac8/report', async (req, res) => {
716
try {
8-
const { creatorAddress, year } = req.params;
9-
10-
if (!creatorAddress || !year) {
11-
return res.status(400).json({
12-
success: false,
13-
error: 'Creator address and year are required'
14-
});
17+
const year = parseYear(req.body.reportingYear);
18+
if (!year) {
19+
return res.status(400).json({ success: false, error: 'reportingYear is required' });
20+
}
21+
22+
const report = await getService(req).generateCarfDac8Report({
23+
reportingYear: year,
24+
jurisdiction: req.body.jurisdiction || 'US',
25+
primaryCurrency: req.body.primaryCurrency || 'USD',
26+
reportingPlatform: req.body.reportingPlatform || {},
27+
generatedBy: req.user?.id || req.body.generatedBy || 'system',
28+
store: req.body.store !== false,
29+
});
30+
31+
return res.status(201).json({ success: true, data: report });
32+
} catch (error) {
33+
console.error('Error generating CARF/DAC8 report:', error);
34+
return res.status(500).json({ success: false, error: 'Failed to generate CARF/DAC8 report' });
35+
}
36+
});
37+
38+
router.get('/admin/reports', (req, res) => {
39+
try {
40+
return res.json({ success: true, data: getService(req).listReports() });
41+
} catch (error) {
42+
console.error('Error listing tax reports:', error);
43+
return res.status(500).json({ success: false, error: 'Failed to list tax reports' });
44+
}
45+
});
46+
47+
router.get('/admin/reports/:reportId', (req, res) => {
48+
try {
49+
const report = getService(req).getReport(req.params.reportId);
50+
if (!report) {
51+
return res.status(404).json({ success: false, error: 'Report not found' });
1552
}
1653

17-
const report = await taxService.generateTaxReport(creatorAddress, parseInt(year));
18-
19-
res.json({
54+
return res.json({
2055
success: true,
21-
data: report
56+
data: {
57+
reportId: report.report_id,
58+
version: report.version,
59+
status: report.status,
60+
payloadHash: report.payload_hash,
61+
previousHash: report.previous_hash,
62+
json: JSON.parse(report.payload_json),
63+
xml: report.payload_xml,
64+
},
2265
});
66+
} catch (error) {
67+
console.error('Error fetching tax report:', error);
68+
return res.status(500).json({ success: false, error: 'Failed to fetch tax report' });
69+
}
70+
});
71+
72+
router.post('/admin/reports/:reportId/sign-off', (req, res) => {
73+
try {
74+
const signer = req.user?.id || req.body.signedOffBy;
75+
if (!signer) {
76+
return res.status(400).json({ success: false, error: 'signedOffBy is required' });
77+
}
78+
79+
const result = getService(req).signOffReport(req.params.reportId, signer, req.body.notes || '');
80+
return res.json({ success: true, data: result });
81+
} catch (error) {
82+
if (error.message === 'Report not found') {
83+
return res.status(404).json({ success: false, error: error.message });
84+
}
85+
console.error('Error signing off tax report:', error);
86+
return res.status(500).json({ success: false, error: 'Failed to sign off tax report' });
87+
}
88+
});
89+
90+
router.get('/report/:creatorAddress/:year', async (req, res) => {
91+
try {
92+
const { creatorAddress, year } = req.params;
93+
const parsedYear = parseYear(year);
94+
if (!creatorAddress || !parsedYear) {
95+
return res.status(400).json({ success: false, error: 'Creator address and year are required' });
96+
}
97+
98+
const report = await getService(req).generateTaxReport(creatorAddress, parsedYear);
99+
return res.json({ success: true, data: report });
23100
} catch (error) {
24101
console.error('Error generating tax report:', error);
25-
res.status(500).json({
26-
success: false,
27-
error: 'Failed to generate tax report'
28-
});
102+
return res.status(500).json({ success: false, error: 'Failed to generate tax report' });
29103
}
30104
});
31105

32-
// Download tax report as CSV
33106
router.get('/csv/:creatorAddress/:year', async (req, res) => {
34107
try {
35108
const { creatorAddress, year } = req.params;
36-
37-
if (!creatorAddress || !year) {
38-
return res.status(400).json({
39-
success: false,
40-
error: 'Creator address and year are required'
41-
});
109+
const parsedYear = parseYear(year);
110+
if (!creatorAddress || !parsedYear) {
111+
return res.status(400).json({ success: false, error: 'Creator address and year are required' });
42112
}
43113

44-
const csvReport = await taxService.generateTaxCSV(creatorAddress, parseInt(year));
45-
114+
const csvReport = await getService(req).generateTaxCSV(creatorAddress, parsedYear);
46115
res.setHeader('Content-Type', 'text/csv');
47116
res.setHeader('Content-Disposition', `attachment; filename="${csvReport.filename}"`);
48-
res.send(csvReport.csvData);
117+
return res.send(csvReport.csvData);
49118
} catch (error) {
50119
console.error('Error generating tax CSV:', error);
51-
res.status(500).json({
52-
success: false,
53-
error: 'Failed to generate tax CSV'
54-
});
120+
return res.status(500).json({ success: false, error: 'Failed to generate tax CSV' });
55121
}
56122
});
57123

58-
// Get tax summary for a creator
59124
router.get('/summary/:creatorAddress/:year', async (req, res) => {
60125
try {
61126
const { creatorAddress, year } = req.params;
62-
63-
if (!creatorAddress || !year) {
64-
return res.status(400).json({
65-
success: false,
66-
error: 'Creator address and year are required'
67-
});
127+
const parsedYear = parseYear(year);
128+
if (!creatorAddress || !parsedYear) {
129+
return res.status(400).json({ success: false, error: 'Creator address and year are required' });
68130
}
69131

70-
const report = await taxService.generateTaxReport(creatorAddress, parseInt(year));
71-
72-
res.json({
132+
const report = await getService(req).generateTaxReport(creatorAddress, parsedYear);
133+
return res.json({
73134
success: true,
74135
data: {
75136
creatorAddress,
76-
year,
137+
year: parsedYear,
77138
summary: report.summary,
78-
generatedAt: report.generatedAt
79-
}
139+
generatedAt: report.generatedAt,
140+
},
80141
});
81142
} catch (error) {
82143
console.error('Error generating tax summary:', error);
83-
res.status(500).json({
84-
success: false,
85-
error: 'Failed to generate tax summary'
86-
});
144+
return res.status(500).json({ success: false, error: 'Failed to generate tax summary' });
87145
}
88146
});
89147

90-
// Get available years for tax reports
91148
router.get('/years/:creatorAddress', async (req, res) => {
92149
try {
93150
const { creatorAddress } = req.params;
94-
95151
if (!creatorAddress) {
96-
return res.status(400).json({
97-
success: false,
98-
error: 'Creator address is required'
99-
});
152+
return res.status(400).json({ success: false, error: 'Creator address is required' });
100153
}
101154

102-
// Get the first transaction date to determine available years
103155
const currentYear = new Date().getFullYear();
104156
const years = [];
105-
106-
// For now, return the last 5 years or since 2020 (whichever is more recent)
107157
const startYear = Math.max(2020, currentYear - 5);
108-
109-
for (let year = startYear; year <= currentYear; year++) {
110-
years.push(year);
111-
}
112-
113-
res.json({
158+
for (let year = startYear; year <= currentYear; year++) years.push(year);
159+
160+
return res.json({
114161
success: true,
115-
data: {
116-
creatorAddress,
117-
availableYears: years,
118-
currentYear
119-
}
162+
data: { creatorAddress, availableYears: years, currentYear },
120163
});
121164
} catch (error) {
122165
console.error('Error fetching available years:', error);
123-
res.status(500).json({
124-
success: false,
125-
error: 'Failed to fetch available years'
126-
});
166+
return res.status(500).json({ success: false, error: 'Failed to fetch available years' });
127167
}
128168
});
129169

130-
// Get fair market value for a specific asset at a specific time
131170
router.get('/fmv/:asset/:timestamp', async (req, res) => {
132171
try {
133172
const { asset, timestamp } = req.params;
134-
135173
if (!asset || !timestamp) {
136-
return res.status(400).json({
137-
success: false,
138-
error: 'Asset and timestamp are required'
139-
});
174+
return res.status(400).json({ success: false, error: 'Asset and timestamp are required' });
140175
}
141176

142-
const fmvData = await taxService.getFairMarketValue(timestamp, asset.toUpperCase());
143-
144-
res.json({
145-
success: true,
146-
data: fmvData
147-
});
177+
const fmvData = await getService(req).getFairMarketValue(timestamp, asset.toUpperCase());
178+
return res.json({ success: true, data: fmvData });
148179
} catch (error) {
149180
console.error('Error fetching fair market value:', error);
150-
res.status(500).json({
151-
success: false,
152-
error: 'Failed to fetch fair market value'
153-
});
181+
return res.status(500).json({ success: false, error: 'Failed to fetch fair market value' });
154182
}
155183
});
156184

0 commit comments

Comments
 (0)