Upload feature (a): BFF endpoints + category config + tests

- Domain/Documents: server-owned category config per wizard + authoritative
  type/size validation (DocumentRules).
- Data/DocumentStore: in-memory metadata store (no file bytes/PII) + audit log;
  user delete (owner-scoped, 409 once linked), admin delete (role seam via
  X-Admin header), link-on-submit, poll-by-localId status.
- Program.cs: GET /uploads/categories, POST /uploads (multipart, excluded from
  OpenAPI — hand-written on FE), GET /uploads/status, DELETE /uploads/{id},
  DELETE /admin/uploads/{id}. Submit links digital docs + records post-delivery.
- Contracts extended (DocumentRefDto on registratie/herregistratie submit);
  regenerated NSwag client + swagger.json (drift check stays green).
- Tests: 16 new (endpoints + DocumentRules); dotnet test 44/44.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 20:15:40 +02:00
parent a079d3259e
commit 0d37cc097a
9 changed files with 813 additions and 5 deletions

View File

@@ -446,6 +446,177 @@ export class ApiClient {
}
return Promise.resolve<ReferentieResponse>(null as any);
}
/**
* @return OK
*/
categories(wizardId: string): Promise<UploadCategoriesDto> {
let url_ = this.baseUrl + "/api/v1/uploads/categories?";
if (wizardId === undefined || wizardId === null)
throw new globalThis.Error("The parameter 'wizardId' must be defined and cannot be null.");
else
url_ += "wizardId=" + encodeURIComponent("" + wizardId) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processCategories(_response);
});
}
protected processCategories(response: Response): Promise<UploadCategoriesDto> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as UploadCategoriesDto;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<UploadCategoriesDto>(null as any);
}
/**
* @param localIds (optional)
* @return OK
*/
status(localIds?: string | undefined): Promise<UploadStatusDto> {
let url_ = this.baseUrl + "/api/v1/uploads/status?";
if (localIds === null)
throw new globalThis.Error("The parameter 'localIds' cannot be null.");
else if (localIds !== undefined)
url_ += "localIds=" + encodeURIComponent("" + localIds) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processStatus(_response);
});
}
protected processStatus(response: Response): Promise<UploadStatusDto> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as UploadStatusDto;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<UploadStatusDto>(null as any);
}
/**
* @return No Content
*/
uploads(documentId: string): Promise<void> {
let url_ = this.baseUrl + "/api/v1/uploads/{documentId}";
if (documentId === undefined || documentId === null)
throw new globalThis.Error("The parameter 'documentId' must be defined.");
url_ = url_.replace("{documentId}", encodeURIComponent("" + documentId));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "DELETE",
headers: {
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processUploads(_response);
});
}
protected processUploads(response: Response): Promise<void> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 204) {
return response.text().then((_responseText) => {
return;
});
} else if (status === 404) {
return response.text().then((_responseText) => {
return throwException("Not Found", status, _responseText, _headers);
});
} else if (status === 409) {
return response.text().then((_responseText) => {
let result409: any = null;
result409 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails;
return throwException("Conflict", status, _responseText, _headers, result409);
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<void>(null as any);
}
/**
* @return No Content
*/
uploads2(documentId: string): Promise<void> {
let url_ = this.baseUrl + "/api/v1/admin/uploads/{documentId}";
if (documentId === undefined || documentId === null)
throw new globalThis.Error("The parameter 'documentId' must be defined.");
url_ = url_.replace("{documentId}", encodeURIComponent("" + documentId));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "DELETE",
headers: {
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processUploads2(_response);
});
}
protected processUploads2(response: Response): Promise<void> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 204) {
return response.text().then((_responseText) => {
return;
});
} else if (status === 403) {
return response.text().then((_responseText) => {
return throwException("Forbidden", status, _responseText, _headers);
});
} else if (status === 404) {
return response.text().then((_responseText) => {
return throwException("Not Found", status, _responseText, _headers);
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<void>(null as any);
}
}
export interface AantekeningDto {
@@ -477,6 +648,23 @@ export interface DashboardViewDto {
decisions?: HerregistratieDecisionsDto;
}
export interface DocumentCategoryDto {
categoryId?: string | undefined;
label?: string | undefined;
description?: string | undefined;
required?: boolean;
acceptedTypes?: string[] | undefined;
maxSizeMb?: number;
multiple?: boolean;
allowPostDelivery?: boolean;
}
export interface DocumentRefDto {
categoryId?: string | undefined;
channel?: string | undefined;
documentId?: string | undefined;
}
export interface DuoDiplomaDto {
id?: string | undefined;
naam?: string | undefined;
@@ -498,6 +686,7 @@ export interface HerregistratieDecisionsDto {
export interface HerregistratieRequest {
uren?: number;
documents?: DocumentRefDto[] | undefined;
}
export interface IntakePolicyDto {
@@ -541,6 +730,7 @@ export interface ReferentieResponse {
export interface RegistratieRequest {
diplomaHerkomst?: string | undefined;
documents?: DocumentRefDto[] | undefined;
}
export interface RegistrationDto {
@@ -560,6 +750,20 @@ export interface RegistrationStatusDto {
doorgehaaldOp?: string | undefined;
}
export interface UploadCategoriesDto {
categories?: DocumentCategoryDto[] | undefined;
}
export interface UploadStatusDto {
results?: UploadStatusItemDto[] | undefined;
}
export interface UploadStatusItemDto {
localId?: string | undefined;
status?: string | undefined;
documentId?: string | undefined;
}
export class SwaggerException extends Error {
override message: string;
status: number;