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
28 changes: 28 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,34 @@ record.
- Added an `adminer` service to `docker-compose.yaml` (port `5555:8080`) for local database management; waits on the `postgres` healthcheck and joins the private application network.
- Simply it added adminer container to docker-compose for DB inspection

## 2026-09-06 - Complete demo user profiles

### Seed and test data

- Expanded the existing test-user seed with complete Amharic profiles, Ethiopian location data, unique phone numbers, and active states for new and existing demo users.
- Ensure contributor/reviewer wallets and linked scores exist without resetting earned balances or scores on reruns. Projects, tasks, assignments, and submissions remain manual UI operations.

## 2026-09-07 - End-to-end workflow corrections

### Validation and submissions

- Added strict nested validation for text submission attempts and compatible validation metadata for annotation/rejection reference-data DTOs.
- Validate prior submissions per microtask with their statuses so rejected work can be retried without accepting duplicate pending/approved work.
- Advance contributor progress by newly submitted microtasks rather than configured batch size; retries do not increment progress or renew deadlines. Added text/audio regression coverage.

### Finance

- Corrected two-decimal withdrawal validation to accept legitimate floating-point amounts such as 0.29.
- Added mocked-provider coverage for reservation, settlement, ambiguous transport errors, insufficient balance, and idempotent failure reversal. Real payouts remain disabled during E2E verification.

## 2026-09-09 - Test submission benchmark fix

### Validation and submissions

- Added the required `Contributor` role and `Pending` status when a contributor
submits a test text dataset, preventing the `UserTask` insert from failing on
its non-null role column.
- Added focused regression coverage for the test text submission path.

## How to record future changes

Expand Down
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,14 +415,45 @@ All routes are served under `/api`.

For exact request and response shapes, use Swagger at `/doc`.

## Test Account
## Test Accounts

```text
Super Admin
username: guest@gmail.com
password: guest@1234
Run these commands from the backend directory against a **local/development database only**.
In `.env`, enable the demo users:

```env
NODE_ENV=development
ENABLE_DEMO_USERS_SEED=true
```

**Native (without Docker):** with dependencies installed and `DATABASE_URL` pointing to a database reachable from your host:

```bash
npm run migration:run
npm run seed
```

**Docker:** rebuild to include the latest seed, then run the compiled scripts:

```bash
docker compose up -d --build
docker compose exec -T app npm run migration:run:prod
docker compose exec -T -e NODE_ENV=development -e ENABLE_DEMO_USERS_SEED=true app npm run seed:prod
```

All demo accounts use password `12345678`:

| Role | Accounts |
| --- | --- |
| SuperAdmin | `super@gmail.com`, `super1@gmail.com` |
| ProjectManager | `proj@gmail.com`, `proj1@gmail.com` |
| Facilitator | `faci@gmail.com`, `faci1@gmail.com` |
| Reviewer | `rev@gmail.com`, `rev2@gmail.com` |
| Contributor (mobile) | `cont@gmail.com`, `cont1@gmail.com` |

For mobile login, enter `123456789` or `234567890` in the phone field; the app adds `+251`.
The seed creates or updates demo profiles, but leaves project/task creation and assignment to the UI.
Never enable demo seeding against a production database.

## Operational Notes

- `NODE_ENV=production` cannot use `CORS_ORIGIN=*`; configure a concrete origin or comma-separated origin list.
Expand Down
98 changes: 98 additions & 0 deletions src/base_data/dto/RejectionType.dto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { ArgumentMetadata, HttpException } from '@nestjs/common';
import { ZodValidationPipe } from 'nestjs-zod';
import { CustomValidationPipe } from 'src/utils/CustomValidationPipe';
import {
CreateRejectionTypeDto,
UpdateRejectionTypeDto,
createRejectionTypeSchema,
} from './RejectionType.dto';

describe('Annotation/rejection reference-data validation', () => {
describe.each([
{
operation: 'create',
metatype: CreateRejectionTypeDto,
type: 'body' as const,
},
{
operation: 'update',
metatype: UpdateRejectionTypeDto,
type: 'body' as const,
},
{
operation: 'query',
metatype: UpdateRejectionTypeDto,
type: 'query' as const,
},
])('$operation', ({ operation, metatype, type }) => {
const metadata: ArgumentMetadata = { metatype, type };
const transform = async (payload: unknown) => {
const validated: unknown = await new CustomValidationPipe().transform(
payload,
metadata,
);
return new ZodValidationPipe().transform(validated, metadata) as unknown;
};

it('preserves known fields through global validation then the route Zod pipe', async () => {
const payload = {
name: 'Quality',
description: 'Reference data for submission review',
};
await expect(transform(payload)).resolves.toEqual(payload);
});

it('allows an omitted description', async () => {
await expect(transform({ name: 'Quality' })).resolves.toEqual({
name: 'Quality',
});
});

it('allows an empty description', async () => {
await expect(
transform({ name: 'Quality', description: '' }),
).resolves.toEqual({ name: 'Quality', description: '' });
});

it.each([
{ name: '' },
{ name: ' \n\t' },
{ name: null },
{ name: 123 },
{ name: ['Quality'] },
{ name: 'Quality', description: null },
{ name: 'Quality', description: 123 },
{ name: 'Quality', unexpected: true },
{ name: 'Quality', created_by: 'client-supplied-user' },
])(
'rejects invalid or unknown fields in %j at the global pipe',
async (payload) => {
await expect(
new CustomValidationPipe().transform(payload, metadata),
).rejects.toBeInstanceOf(HttpException);
},
);

it('requires name only for create', async () => {
for (const payload of [{}, { description: 'Updated description' }]) {
if (operation === 'create') {
await expect(transform(payload)).rejects.toBeInstanceOf(
HttpException,
);
} else {
await expect(transform(payload)).resolves.toEqual(payload);
}
}
});
});

it('keeps the exported create schema consistent with required nonblank names', () => {
expect(createRejectionTypeSchema.safeParse({}).success).toBe(false);
expect(createRejectionTypeSchema.safeParse({ name: ' ' }).success).toBe(
false,
);
expect(
createRejectionTypeSchema.safeParse({ name: 'Quality' }).success,
).toBe(true);
});
});
27 changes: 22 additions & 5 deletions src/base_data/dto/RejectionType.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { IsString, Matches, ValidateIf } from 'class-validator';
import { createZodDto } from 'nestjs-zod';
import { PaginationDto } from 'src/common/dto/Pagination.dto';
import { z } from 'zod';
Expand All @@ -16,13 +16,30 @@ export class CreateFlagTypeDto extends PaginationDto {
description?: string;
}
export const createRejectionTypeSchema = z.object({
name: z.string().min(1).optional(),
name: z.string().regex(/\S/, 'name must contain non-whitespace text'),
description: z.string().optional(),
});

export class CreateRejectionTypeDto extends createZodDto(
createRejectionTypeSchema,
) {}
) {
@IsString()
@Matches(/\S/, { message: 'name must contain non-whitespace text' })
name: string;

@ValidateIf((_object, value: unknown) => value !== undefined)
@IsString()
description?: string;
}
export class UpdateRejectionTypeDto extends createZodDto(
createRejectionTypeSchema,
) {}
createRejectionTypeSchema.partial(),
) {
@ValidateIf((_object, value: unknown) => value !== undefined)
@IsString()
@Matches(/\S/, { message: 'name must contain non-whitespace text' })
name?: string;

@ValidateIf((_object, value: unknown) => value !== undefined)
@IsString()
description?: string;
}
Loading
Loading