Milestone 4: mock retrieval receipt JSON (extra seal)
GET /zapier/v1/receipts/{jobId}?format=json returns a retrieval receipt
with extraSeal.event=document.retrieved. PDF format is 501 until legal
copy exists. OPEN.md lists work that needs Zapier login or live Verae.
This commit is contained in:
parent
51ae79b75f
commit
c8f9d01a04
5 changed files with 129 additions and 1 deletions
|
|
@ -12,7 +12,7 @@ Source: [workspace-brief.md](../00-sources/workspace-brief.md). Owners after com
|
||||||
| f | e + encrypted LTS binary | verae mock | Missing. Do not use zappier demo `/v1/storage` | No |
|
| f | e + encrypted LTS binary | verae mock | Missing. Do not use zappier demo `/v1/storage` | No |
|
||||||
| g | Share file/dir + decryption key | verae mock | Missing | No |
|
| g | Share file/dir + decryption key | verae mock | Missing | No |
|
||||||
| h | Retrieve SHA256 + metadata | verae status/verify | Partial by `jobId` | `GET /api/status/{jobId}`, `GET /api/verify/{jobId}` |
|
| h | Retrieve SHA256 + metadata | verae status/verify | Partial by `jobId` | `GET /api/status/{jobId}`, `GET /api/verify/{jobId}` |
|
||||||
| i | Certified retrieval receipt (PDF/JSON) extra seal | verae mock | Missing. Not a payment invoice | No |
|
| i | Certified retrieval receipt (PDF/JSON) extra seal | `GET /zapier/v1/receipts/{jobId}?format=json` | **JSON mock done**; PDF 501 | No |
|
||||||
| j | Setup payment method | zappier portal reload (Stripe) | Built (prepaid reload; card-on-file TBD) | No |
|
| j | Setup payment method | zappier portal reload (Stripe) | Built (prepaid reload; card-on-file TBD) | No |
|
||||||
| k | Get invoices | zappier portal + admin | Built | No |
|
| k | Get invoices | zappier portal + admin | Built | No |
|
||||||
| l | Get payment receipts | zappier invoice HTML/print | Built as invoices | No |
|
| l | Get payment receipts | zappier invoice HTML/print | Built as invoices | No |
|
||||||
|
|
|
||||||
11
docs/OPEN.md
Normal file
11
docs/OPEN.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Still open (needs you or live Verae)
|
||||||
|
|
||||||
|
These cannot finish in this session:
|
||||||
|
|
||||||
|
1. **`zapier-platform login` + `register` + `push`** — browser OAuth to your Zapier account. Follow [docs/04-activate/SETUP-ZAPIER-DEVELOPER.md](04-activate/SETUP-ZAPIER-DEVELOPER.md). After push, commit `.zapierapprc`.
|
||||||
|
2. **Live `api.veraetime.net`** — hash lookup, metadata, receipts, LTS, share are **not** in the public OpenAPI. Mocks exist; live client refuses unknown paths.
|
||||||
|
3. **PDF receipts** — JSON extra-seal works; PDF is 501.
|
||||||
|
4. **Encrypted LTS + directory share (f, g)** — not implemented beyond mock timestamp metadata.
|
||||||
|
5. **NS1 NATS public bind** — left loopback-only; use SSH tunnel (`14222`).
|
||||||
|
|
||||||
|
Everything else for morning Add Numbers is ready.
|
||||||
|
|
@ -11,6 +11,7 @@ import { statusRoutes } from './statusRoutes.js';
|
||||||
import { webhookRoutes } from './webhookRoutes.js';
|
import { webhookRoutes } from './webhookRoutes.js';
|
||||||
import { publicTenantRoutes, adminTenantRoutes } from './tenantRoutes.js';
|
import { publicTenantRoutes, adminTenantRoutes } from './tenantRoutes.js';
|
||||||
import { hashRoutes } from './hashRoutes.js';
|
import { hashRoutes } from './hashRoutes.js';
|
||||||
|
import { receiptRoutes } from './receiptRoutes.js';
|
||||||
import { authenticate } from '../middleware/authenticate.js';
|
import { authenticate } from '../middleware/authenticate.js';
|
||||||
import { rateLimit } from '../middleware/rateLimit.js';
|
import { rateLimit } from '../middleware/rateLimit.js';
|
||||||
|
|
||||||
|
|
@ -31,5 +32,6 @@ protectedRoutes.use('/verify', verifyRoutes);
|
||||||
protectedRoutes.use('/status', statusRoutes);
|
protectedRoutes.use('/status', statusRoutes);
|
||||||
protectedRoutes.use('/webhooks', webhookRoutes);
|
protectedRoutes.use('/webhooks', webhookRoutes);
|
||||||
protectedRoutes.use('/hashes', hashRoutes);
|
protectedRoutes.use('/hashes', hashRoutes);
|
||||||
|
protectedRoutes.use('/receipts', receiptRoutes);
|
||||||
|
|
||||||
apiRoutes.use('/v1', protectedRoutes);
|
apiRoutes.use('/v1', protectedRoutes);
|
||||||
|
|
|
||||||
43
packages/verae-zapier-middleware/src/routes/receiptRoutes.js
Normal file
43
packages/verae-zapier-middleware/src/routes/receiptRoutes.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* Mock certified retrieval receipt (JSON). Not a payment invoice.
|
||||||
|
* @module routes/receiptRoutes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router } from 'express';
|
||||||
|
import { asyncHandler, AppError } from '../errors.js';
|
||||||
|
import { getJobStatus } from '../services/timestampService.js';
|
||||||
|
|
||||||
|
export const receiptRoutes = Router();
|
||||||
|
|
||||||
|
receiptRoutes.get(
|
||||||
|
'/:jobId',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const { jobId } = req.params;
|
||||||
|
const format = String(req.query.format || 'json').toLowerCase();
|
||||||
|
const status = await getJobStatus(req.auth, jobId);
|
||||||
|
if (status.status !== 'completed') {
|
||||||
|
throw new AppError('Receipt only for completed jobs', { status: 409, code: 'NOT_COMPLETE' });
|
||||||
|
}
|
||||||
|
const retrievedAt = new Date().toISOString();
|
||||||
|
const receipt = {
|
||||||
|
type: 'verae.retrieval-receipt',
|
||||||
|
format: 'json',
|
||||||
|
jobId,
|
||||||
|
sha256: status.sha256 ?? status.metadata?.sha256,
|
||||||
|
timestamp: status.completedAt ?? status.metadata?.timestamp,
|
||||||
|
certificate: status.result,
|
||||||
|
extraSeal: {
|
||||||
|
event: 'document.retrieved',
|
||||||
|
retrievedAt,
|
||||||
|
tenantId: req.auth.tenantId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (format === 'pdf') {
|
||||||
|
throw new AppError('PDF receipts not implemented; use format=json', {
|
||||||
|
status: 501,
|
||||||
|
code: 'NOT_IMPLEMENTED',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.json(receipt);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { describe, it, before, after } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import http from 'node:http';
|
||||||
|
import { createApp } from '../../src/app.js';
|
||||||
|
import { useTempStore, seedProTenant } from '../helpers.js';
|
||||||
|
import { clearMockJobs } from '../../src/clients/veraeClient.js';
|
||||||
|
import { config } from '../../src/config.js';
|
||||||
|
|
||||||
|
describe('retrieval receipt JSON', () => {
|
||||||
|
let ctx;
|
||||||
|
let server;
|
||||||
|
let port;
|
||||||
|
let apiKey;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
config.mockVerae = true;
|
||||||
|
config.natsEnabled = false;
|
||||||
|
config.mockJobCompleteMs = 50;
|
||||||
|
clearMockJobs();
|
||||||
|
ctx = useTempStore();
|
||||||
|
apiKey = seedProTenant().apiKey;
|
||||||
|
const app = createApp();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
server = app.listen(0, '127.0.0.1', () => {
|
||||||
|
port = server.address().port;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await new Promise((r) => server.close(r));
|
||||||
|
ctx.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('issues a JSON extra-seal receipt for a completed job', async () => {
|
||||||
|
const created = await call(port, 'POST', '/zapier/v1/timestamp/wait', apiKey, { data: 'receipt-me' });
|
||||||
|
assert.equal(created.body.status, 'completed');
|
||||||
|
const jobId = created.body.id;
|
||||||
|
const rec = await call(port, 'GET', `/zapier/v1/receipts/${jobId}?format=json`, apiKey);
|
||||||
|
assert.equal(rec.status, 200);
|
||||||
|
assert.equal(rec.body.type, 'verae.retrieval-receipt');
|
||||||
|
assert.equal(rec.body.extraSeal.event, 'document.retrieved');
|
||||||
|
assert.equal(rec.body.jobId, jobId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function call(port, method, path, apiKey, body) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = http.request(
|
||||||
|
{
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port,
|
||||||
|
path,
|
||||||
|
method,
|
||||||
|
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (c) => {
|
||||||
|
data += c;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
resolve({ status: res.statusCode, body: data ? JSON.parse(data) : {} });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
req.on('error', reject);
|
||||||
|
if (body) req.write(JSON.stringify(body));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue