feat(expense-snapshot): frontend model + api service with tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Chris Chen
2026-06-25 15:11:32 -07:00
parent 8922bb69de
commit bc827e8b60
3 changed files with 103 additions and 0 deletions
@@ -0,0 +1,21 @@
import { ExpenseLineInput, FunctionalClass } from './expense.model';
export interface ExpenseSnapshotLineDto {
categoryGroupId: number; categoryGroupName: string;
subCategoryId: number; subCategoryName: string;
functionalClass: FunctionalClass | null; amount: number; description: string | null;
}
export interface ExpenseSnapshotDto {
id: number; name: string; ministryId: number; ministryName: string;
description: string; vendorName: string | null; checkNumber: string | null; notes: string | null;
totalAmount: number; lineCount: number;
createdByName: string | null; createdAt: string;
lines: ExpenseSnapshotLineDto[];
}
export interface CreateExpenseSnapshotRequest {
name: string; ministryId: number; lines: ExpenseLineInput[];
description: string; vendorName: string | null; checkNumber: string | null; notes: string | null;
}
export type UpdateExpenseSnapshotRequest = CreateExpenseSnapshotRequest;
@@ -0,0 +1,52 @@
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ExpenseSnapshotApiService } from './expense-snapshot-api.service';
import { ApiConfigService } from '../../../core/services/api-config.service';
import { CreateExpenseSnapshotRequest } from '../models/expense-snapshot.model';
describe('ExpenseSnapshotApiService', () => {
let service: ExpenseSnapshotApiService;
let httpMock: HttpTestingController;
const base = 'http://test/api/expense-snapshots';
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
ExpenseSnapshotApiService,
{ provide: ApiConfigService, useValue: { getApiUrl: () => base } },
],
});
service = TestBed.inject(ExpenseSnapshotApiService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('getAll() GETs the collection endpoint', () => {
service.getAll().subscribe();
const req = httpMock.expectOne(base);
expect(req.request.method).toBe('GET');
req.flush([]);
});
it('create() POSTs the request body', () => {
const body: CreateExpenseSnapshotRequest = {
name: 'Rent', ministryId: 1, description: 'Office rent',
vendorName: 'Landlord X', checkNumber: null, notes: null,
lines: [{ categoryGroupId: 1, subCategoryId: 1, amount: 1200, functionalClass: null, description: null }],
};
service.create(body).subscribe();
const req = httpMock.expectOne(base);
expect(req.request.method).toBe('POST');
expect(req.request.body.name).toBe('Rent');
req.flush({ id: 7 });
});
it('delete() DELETEs by id', () => {
service.delete(9).subscribe();
const req = httpMock.expectOne(`${base}/9`);
expect(req.request.method).toBe('DELETE');
req.flush(null);
});
});
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ApiConfigService } from '../../../core/services/api-config.service';
import {
ExpenseSnapshotDto, CreateExpenseSnapshotRequest, UpdateExpenseSnapshotRequest,
} from '../models/expense-snapshot.model';
@Injectable({ providedIn: 'root' })
export class ExpenseSnapshotApiService {
private readonly endpoint: string;
constructor(private http: HttpClient, apiConfig: ApiConfigService) {
this.endpoint = apiConfig.getApiUrl('expense-snapshots');
}
getAll(): Observable<ExpenseSnapshotDto[]> {
return this.http.get<ExpenseSnapshotDto[]>(this.endpoint);
}
getById(id: number): Observable<ExpenseSnapshotDto> {
return this.http.get<ExpenseSnapshotDto>(`${this.endpoint}/${id}`);
}
create(r: CreateExpenseSnapshotRequest): Observable<{ id: number }> {
return this.http.post<{ id: number }>(this.endpoint, r);
}
update(id: number, r: UpdateExpenseSnapshotRequest): Observable<void> {
return this.http.put<void>(`${this.endpoint}/${id}`, r);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.endpoint}/${id}`);
}
}