Skip to content
Draft
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
3 changes: 3 additions & 0 deletions apps/api/drizzle/0027_recommended_lists.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Listas de recomendación y "qué sí llevar" para templates y emergencias.
ALTER TABLE templates ADD COLUMN recommended_list text[] NOT NULL DEFAULT '{}';
ALTER TABLE emergencies ADD COLUMN recommended_list text[] NOT NULL DEFAULT '{}';
45 changes: 41 additions & 4 deletions apps/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5861,12 +5861,25 @@
"type": "string",
"example": "No se aceptan mascotas en el centro de acopio.",
"nullable": true
},
"recommendedList": {
"example": [
"agua",
"dieta líquida",
"ítems EV"
],
"description": "Items and priorities people SHOULD bring",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"name",
"description",
"dontBringList"
"dontBringList",
"recommendedList"
]
},
"CreateTemplateResponseDto": {
Expand Down Expand Up @@ -5914,6 +5927,16 @@
"createdAt": {
"type": "string",
"example": "2026-06-27T10:00:00.000Z"
},
"recommendedList": {
"example": [
"agua",
"dieta líquida"
],
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
Expand All @@ -5922,7 +5945,8 @@
"description",
"dontBringList",
"defaultAnnouncement",
"createdAt"
"createdAt",
"recommendedList"
]
},
"CreateEmergencyDto": {
Expand Down Expand Up @@ -6016,6 +6040,18 @@
"updatedAt": {
"type": "string",
"example": "2026-06-25T10:00:00.000Z"
},
"recommendedList": {
"example": [
"agua",
"dieta líquida",
"ítems EV"
],
"description": "Items and priorities people SHOULD bring to the emergency",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
Expand All @@ -6026,7 +6062,8 @@
"status",
"announcement",
"dontBringList",
"updatedAt"
"updatedAt",
"recommendedList"
]
},
"CreateEmergencyFromTemplateDto": {
Expand Down Expand Up @@ -8061,4 +8098,4 @@
}
}
}
}
}
14 changes: 12 additions & 2 deletions apps/api/scripts/seed-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const DONT_BRING_LIST = [
'Personas sin formación sanitaria mínima en zonas de atención',
];

const RECOMMENDED_LIST = [
'Agua potable y alimento de fácil consumo',
'Documento de identidad y tarjeta sanitaria',
'Medicamentos personales con receta o prospecto',
'Ropa de recambio y cargadores portátiles',
'Mascarillas, guantes y gel hidroalcohólico',
];

const DEFAULT_ANNOUNCEMENT =
'Activado protocolo de emergencia sanitaria. Coordinamos necesidades de ' +
'medicamentos, equipos e insumos médicos. Solo personal sanitario ' +
Expand All @@ -46,6 +54,7 @@ async function seed(): Promise<void> {
'Plantilla para emergencias del vertical sanitario: hospitales, ' +
'refugios médicos y situaciones que requieren taxonomía médica.',
dontBringList: DONT_BRING_LIST,
recommendedList: RECOMMENDED_LIST,
defaultAnnouncement: DEFAULT_ANNOUNCEMENT,
createdAt: new Date(),
})
Expand All @@ -54,9 +63,10 @@ async function seed(): Promise<void> {
set: {
name: 'Emergencia sanitaria',
description:
'Plantilla para emergencias del vertical sanitario: hospitales, ' +
'refugios médicos y situaciones que requieren taxonomía médica.',
'Plantilla para emergencias del vertical sanitario: hospitales, ' +
'refugios médicos y situaciones que requieren taxonomía médica.',
dontBringList: DONT_BRING_LIST,
recommendedList: RECOMMENDED_LIST,
defaultAnnouncement: DEFAULT_ANNOUNCEMENT,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,26 @@ const TEMPLATE_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
describe('CreateEmergencyFromTemplate', () => {
function makeTemplate(opts: {
dontBringList?: string[];
recommendedList?: string[];
defaultAnnouncement?: string | null;
}) {
return Template.create({
id: TemplateId.fromString(TEMPLATE_ID),
name: 'Terremoto básico',
description: 'Template de prueba',
dontBringList: opts.dontBringList ?? [],
recommendedList: opts.recommendedList ?? [],
defaultAnnouncement: opts.defaultAnnouncement ?? null,
});
}

it('creates an emergency copying dontBringList and announcement from template', async () => {
it('creates an emergency copying lists and announcement from template', async () => {
const emergencyRepo = new InMemoryEmergencyRepository();
const templateRepo = new InMemoryTemplateRepository();
await templateRepo.save(
makeTemplate({
dontBringList: ['mascotas', 'joyas'],
recommendedList: ['agua', 'dieta líquida'],
defaultAnnouncement: 'No traer mascotas',
}),
);
Expand All @@ -52,14 +55,19 @@ describe('CreateEmergencyFromTemplate', () => {
);
expect(created).not.toBeNull();
expect(created?.dontBringList).toEqual(['mascotas', 'joyas']);
expect(created?.recommendedList).toEqual(['agua', 'dieta líquida']);
expect(created?.announcement).toBe('No traer mascotas');
});

it('works when template has null defaultAnnouncement', async () => {
const emergencyRepo = new InMemoryEmergencyRepository();
const templateRepo = new InMemoryTemplateRepository();
await templateRepo.save(
makeTemplate({ dontBringList: [], defaultAnnouncement: null }),
makeTemplate({
dontBringList: [],
recommendedList: [],
defaultAnnouncement: null,
}),
);

const useCase = new CreateEmergencyFromTemplate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class CreateEmergencyFromTemplate {
slug,
country: cmd.country,
dontBringList: template.dontBringList,
recommendedList: template.recommendedList,
announcement: template.defaultAnnouncement,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface EmergencyView {
status: string;
announcement: string | null;
dontBringList: string[];
recommendedList: string[];
updatedAt: string;
}

Expand All @@ -20,6 +21,7 @@ export function toEmergencyView(e: Emergency): EmergencyView {
status: e.status,
announcement: e.announcement,
dontBringList: e.dontBringList,
recommendedList: e.recommendedList,
updatedAt: e.updatedAt.toISOString(),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ describe('ListActiveEmergencies', () => {
slug: 'closed-relief',
country: 'ES',
status: EmergencyStatus.Closed,
announcement: null,
dontBringList: [],
recommendedList: [],
createdAt: new Date(),
updatedAt: new Date(),
});
await repo.save(closed);

Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/contexts/emergencies/domain/emergency.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('Emergency', () => {
it('creates with null announcement and updatedAt equal to createdAt', () => {
const e = makeEmergency();
expect(e.announcement).toBeNull();
expect(e.recommendedList).toEqual([]);
expect(e.updatedAt.toISOString()).toBe(e.createdAt.toISOString());
});

Expand Down Expand Up @@ -132,6 +133,7 @@ describe('Emergency', () => {
expect(restored.slug.equals(e.slug)).toBe(true);
expect(restored.status).toBe(EmergencyStatus.Paused);
expect(restored.announcement).toBe('Round-trip test');
expect(restored.recommendedList).toEqual([]);
expect(restored.updatedAt.toISOString()).toBe(e.updatedAt.toISOString());
expect(restored.country).toBe('TR');
expect(restored.createdAt.toISOString()).toBe(e.createdAt.toISOString());
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/contexts/emergencies/domain/emergency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface CreateEmergencyProps {
slug: Slug;
country: string;
dontBringList?: string[];
recommendedList?: string[];
announcement?: string | null;
}

Expand All @@ -20,6 +21,7 @@ export interface EmergencySnapshot {
status: EmergencyStatus;
announcement: string | null;
dontBringList: string[];
recommendedList: string[];
createdAt: Date;
updatedAt: Date;
}
Expand All @@ -33,6 +35,7 @@ export class Emergency {
private _status: EmergencyStatus,
private _announcement: string | null,
private _dontBringList: string[],
private _recommendedList: string[],
public readonly createdAt: Date,
private _updatedAt: Date,
) {}
Expand All @@ -47,6 +50,7 @@ export class Emergency {
EmergencyStatus.Active,
props.announcement ?? null,
props.dontBringList ?? [],
props.recommendedList ?? [],
now,
now,
);
Expand All @@ -61,6 +65,7 @@ export class Emergency {
snap.status,
snap.announcement,
snap.dontBringList,
snap.recommendedList,
snap.createdAt,
snap.updatedAt,
);
Expand All @@ -78,6 +83,10 @@ export class Emergency {
return this._dontBringList;
}

get recommendedList(): string[] {
return this._recommendedList;
}

get updatedAt(): Date {
return this._updatedAt;
}
Expand Down Expand Up @@ -117,6 +126,7 @@ export class Emergency {
status: this._status,
announcement: this._announcement,
dontBringList: this._dontBringList,
recommendedList: this._recommendedList,
createdAt: this.createdAt,
updatedAt: this._updatedAt,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('DrizzleEmergencyRepository (integration)', () => {
name: 'Emergencia sísmica — Venezuela',
slug: Slug.fromString('venezuela'),
country: 'VE',
recommendedList: ['agua', 'dieta líquida'],
});

await repo.save(emergency);
Expand All @@ -47,6 +48,7 @@ describe('DrizzleEmergencyRepository (integration)', () => {
expect(found?.slug.value).toBe('venezuela');
expect(found?.country).toBe('VE');
expect(found?.status).toBe(EmergencyStatus.Active);
expect(found?.recommendedList).toEqual(['agua', 'dieta líquida']);
});

it('findBySlug returns the correct emergency', async () => {
Expand All @@ -55,6 +57,7 @@ describe('DrizzleEmergencyRepository (integration)', () => {
name: 'Emergencia sísmica — Venezuela',
slug: Slug.fromString('venezuela'),
country: 'VE',
recommendedList: ['agua'],
});

await repo.save(emergency);
Expand All @@ -75,13 +78,15 @@ describe('DrizzleEmergencyRepository (integration)', () => {
name: 'Active Emergency',
slug: Slug.fromString('active-emergency'),
country: 'VE',
recommendedList: ['agua'],
});

const closed = Emergency.create({
id: EmergencyId.fromString('22222222-2222-4222-8222-222222222222'),
name: 'Closed Emergency',
slug: Slug.fromString('closed-emergency'),
country: 'CO',
recommendedList: ['agua'],
});
closed.close();

Expand All @@ -101,12 +106,14 @@ describe('DrizzleEmergencyRepository (integration)', () => {
slug: Slug.fromString('dont-bring-test'),
country: 'VE',
dontBringList: ['mascotas', 'joyas'],
recommendedList: ['agua', 'dieta líquida'],
});

await repo.save(emergency);
const found = await repo.findById(emergency.id);

expect(found).not.toBeNull();
expect(found?.dontBringList).toEqual(['mascotas', 'joyas']);
expect(found?.recommendedList).toEqual(['agua', 'dieta líquida']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function rowToSnapshot(row: Row): EmergencySnapshot {
status: row.status as EmergencyStatus,
announcement: row.announcement ?? null,
dontBringList: row.dontBringList,
recommendedList: row.recommendedList,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
Expand All @@ -38,6 +39,7 @@ export class DrizzleEmergencyRepository implements EmergencyRepository {
status: s.status,
announcement: s.announcement,
dontBringList: s.dontBringList,
recommendedList: s.recommendedList,
createdAt: s.createdAt,
updatedAt: s.updatedAt,
})
Expand All @@ -49,6 +51,7 @@ export class DrizzleEmergencyRepository implements EmergencyRepository {
country: s.country,
announcement: s.announcement,
dontBringList: s.dontBringList,
recommendedList: s.recommendedList,
updatedAt: s.updatedAt,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const emergenciesTable = pgTable('emergencies', {
status: text('status').notNull(),
announcement: text('announcement'),
dontBringList: text('dont_bring_list').array().notNull().default([]),
recommendedList: text('recommended_list').array().notNull().default([]),
createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export class EmergencyViewDto {
})
dontBringList!: string[];

@ApiProperty({
example: ['agua', 'dieta líquida', 'ítems EV'],
type: [String],
description: 'Items and priorities people SHOULD bring to the emergency',
})
recommendedList!: string[];

@ApiProperty({ example: '2026-06-25T10:00:00.000Z' })
updatedAt!: string;
}
Expand Down
Loading