@flowselections/floriday-voorraad 1.0.137 → 1.0.141
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
- package/dist-lib/components/voorraad/ArtikelWizard.js +9 -7
- package/dist-lib/components/voorraad/StockMutationHistoryCard.d.ts.map +1 -1
- package/dist-lib/components/voorraad/StockMutationHistoryCard.js +19 -4
- package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
- package/dist-lib/components/voorraad/VoorraadPage.js +2 -5
- package/dist-lib/components/voorraad/VoorraadStockCard.d.ts.map +1 -1
- package/dist-lib/components/voorraad/VoorraadStockCard.js +69 -47
- package/dist-lib/hooks/useTradeItemStock.d.ts +6 -1
- package/dist-lib/hooks/useTradeItemStock.d.ts.map +1 -1
- package/dist-lib/hooks/useTradeItemStock.js +32 -44
- package/dist-lib/integrations/supabase/types.d.ts +416 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/__tests__/release-140.test.d.ts +2 -0
- package/dist-lib/lib/__tests__/release-140.test.d.ts.map +1 -0
- package/dist-lib/lib/__tests__/release-140.test.js +42 -0
- package/dist-lib/lib/__tests__/stock-save.test.d.ts +2 -0
- package/dist-lib/lib/__tests__/stock-save.test.d.ts.map +1 -0
- package/dist-lib/lib/__tests__/stock-save.test.js +35 -0
- package/dist-lib/lib/db-error-message.d.ts +7 -0
- package/dist-lib/lib/db-error-message.d.ts.map +1 -0
- package/dist-lib/lib/db-error-message.js +36 -0
- package/dist-lib/lib/stock-save.d.ts +43 -0
- package/dist-lib/lib/stock-save.d.ts.map +1 -0
- package/dist-lib/lib/stock-save.js +45 -0
- package/package.json +3 -1
- package/sql/AGENTS.md +3 -0
- package/sql/floriday-voorraad-schema.sql +301 -77
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-140.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/release-140.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { opslaanMelding, valuesAfterSave, OPSLAAN_MISLUKT_TEKST } from "../stock-save.js";
|
|
4
|
+
import { dbFoutTekst } from "../db-error-message.js";
|
|
5
|
+
// @ts-expect-error plain ESM script
|
|
6
|
+
import { controleerSchema } from "../../../scripts/check-schema-version.mjs";
|
|
7
|
+
const saved = { physical_stock: 10, minimum_stock: 0, reserved_stock: 2, incoming_stock: 0, notes: null };
|
|
8
|
+
const entered = { ...saved, physical_stock: 100 };
|
|
9
|
+
describe("1.0.140 opslaan bij dichte poort", () => {
|
|
10
|
+
it("bevestigde opslag met dichte poort: 'Opgeslagen; er is niets naar Floriday gestuurd.'", () => {
|
|
11
|
+
const m = opslaanMelding({ opgeslagen: true, floriday_klaargezet_toegestaan: false }, true);
|
|
12
|
+
expect(m).toEqual({ ok: true, tekst: "Opgeslagen; er is niets naar Floriday gestuurd." });
|
|
13
|
+
});
|
|
14
|
+
it("teruggedraaide opslag (fout, geen antwoord) = mislukt en oude waarden terug", () => {
|
|
15
|
+
const m = opslaanMelding(null, true);
|
|
16
|
+
expect(m).toEqual({ ok: false, tekst: OPSLAAN_MISLUKT_TEKST });
|
|
17
|
+
expect(valuesAfterSave(m.ok, entered, saved).physical_stock).toBe(10);
|
|
18
|
+
});
|
|
19
|
+
it("antwoord zonder bevestiging telt niet als opgeslagen", () => {
|
|
20
|
+
expect(opslaanMelding({ opgeslagen: false }, true).ok).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe("1.0.140 databasefouten in gewone taal", () => {
|
|
24
|
+
it("ontbrekende controlewaarde wordt 'nog niet beschikbaar'", () => {
|
|
25
|
+
const t = dbFoutTekst({ code: "23514", message: 'new row for relation "stock_mutation_log" violates check constraint' }, "Afboeken");
|
|
26
|
+
expect(t).toBe("Afboeken is nog niet beschikbaar in deze omgeving, neem contact op met de beheerder.");
|
|
27
|
+
});
|
|
28
|
+
it("oude poortfout toont geen technische tekst", () => {
|
|
29
|
+
const t = dbFoutTekst({ message: "Uitgaand Floriday-verkeer (stock_publish) is uitgeschakeld" }, "Opslaan");
|
|
30
|
+
expect(t).not.toContain("stock_publish");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("1.0.140 schemacontrole", () => {
|
|
34
|
+
it("ouder schema blokkeert", () => {
|
|
35
|
+
expect(controleerSchema("-- @flowselections/floriday-voorraad 1.0.136 x", "1.0.140").length).toBeGreaterThan(0);
|
|
36
|
+
});
|
|
37
|
+
it("het meegeleverde schema hoort bij de versie in package.json", () => {
|
|
38
|
+
const versie = JSON.parse(readFileSync("package.json", "utf8")).version;
|
|
39
|
+
const sql = readFileSync("sql/floriday-voorraad-schema.sql", "utf8");
|
|
40
|
+
expect(controleerSchema(sql, versie)).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stock-save.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/stock-save.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { afboekFout, floridayNaOpslaanTekst, valuesAfterSave } from "../stock-save.js";
|
|
3
|
+
const saved = { physical_stock: 40, minimum_stock: 5, reserved_stock: 12, incoming_stock: 0, notes: null };
|
|
4
|
+
describe("lokaal voorraad opslaan (1.0.138)", () => {
|
|
5
|
+
it("mislukte opslag zet de oude waarden terug (geen 'Beschikbaar: 100' zonder opslag)", () => {
|
|
6
|
+
const entered = { ...saved, physical_stock: 112 };
|
|
7
|
+
expect(valuesAfterSave(false, entered, saved).physical_stock).toBe(40);
|
|
8
|
+
});
|
|
9
|
+
it("gereserveerd komt na opslaan uit de opgeslagen rij, niet uit invoer", () => {
|
|
10
|
+
const entered = { ...saved, physical_stock: 100, reserved_stock: 0 };
|
|
11
|
+
const out = valuesAfterSave(true, entered, saved);
|
|
12
|
+
expect(out.physical_stock).toBe(100);
|
|
13
|
+
expect(out.reserved_stock).toBe(12);
|
|
14
|
+
});
|
|
15
|
+
it("Floriday-artikel bij dichte poort: lokaal opgeslagen, melding dat er niets naar Floriday gaat", () => {
|
|
16
|
+
expect(floridayNaOpslaanTekst(true, false)).toMatch(/niets naar Floriday/);
|
|
17
|
+
expect(floridayNaOpslaanTekst(true, null)).toMatch(/niets naar Floriday/);
|
|
18
|
+
expect(floridayNaOpslaanTekst(true, true)).toBeNull();
|
|
19
|
+
expect(floridayNaOpslaanTekst(false, false)).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("afboeken met verplichte reden", () => {
|
|
23
|
+
it("zonder reden geweigerd", () => {
|
|
24
|
+
expect(afboekFout(5, null, 40)).toBe("Kies een reden.");
|
|
25
|
+
expect(afboekFout(5, "zomaar", 40)).toBe("Kies een reden.");
|
|
26
|
+
});
|
|
27
|
+
it("met reden uit de vaste lijst toegestaan", () => {
|
|
28
|
+
for (const r of ["derving", "kapot", "eigen_gebruik", "overig"])
|
|
29
|
+
expect(afboekFout(5, r, 40)).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
it("niet meer dan liggend, en groter dan 0", () => {
|
|
32
|
+
expect(afboekFout(41, "kapot", 40)).toMatch(/niet meer afboeken/);
|
|
33
|
+
expect(afboekFout(0, "kapot", 40)).toMatch(/groter dan 0/);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Databasefouten nooit letterlijk tonen (1.0.140). De technische tekst gaat
|
|
3
|
+
* alleen naar de console; de gebruiker krijgt gewone taal.
|
|
4
|
+
*/
|
|
5
|
+
export type DbFoutActie = "Opslaan" | "Afboeken" | "Geschiedenis laden" | "Deze actie";
|
|
6
|
+
export declare function dbFoutTekst(e: unknown, actie?: DbFoutActie): string;
|
|
7
|
+
//# sourceMappingURL=db-error-message.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-error-message.d.ts","sourceRoot":"","sources":["../../src/lib/db-error-message.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,oBAAoB,GAAG,YAAY,CAAC;AAmBvF,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,GAAE,WAA0B,GAAG,MAAM,CA0BjF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Eigen Nederlandse meldingen uit onze functies (ERRCODE 22023/P0002/42501) mogen wel door. */
|
|
2
|
+
const EIGEN_MELDINGEN = [
|
|
3
|
+
/^Kies een reden/i,
|
|
4
|
+
/^Vul een (aantal|voorraadaantal)/i,
|
|
5
|
+
/^Je kunt niet meer afboeken/i,
|
|
6
|
+
/^Voorraadaantallen mogen niet negatief/i,
|
|
7
|
+
/^Geen voorraadregel/i,
|
|
8
|
+
/^Je hebt geen schrijfrechten/i,
|
|
9
|
+
/^Onvoldoende voorraad/i,
|
|
10
|
+
];
|
|
11
|
+
export function dbFoutTekst(e, actie = "Deze actie") {
|
|
12
|
+
const err = (e ?? {});
|
|
13
|
+
const raw = String(err.message ?? (e instanceof Error ? e.message : e ?? ""));
|
|
14
|
+
const code = String(err.code ?? "");
|
|
15
|
+
const lower = raw.toLowerCase();
|
|
16
|
+
if (typeof console !== "undefined")
|
|
17
|
+
console.warn(`[voorraad] ${actie} mislukt:`, raw, code);
|
|
18
|
+
const eigen = EIGEN_MELDINGEN.find((re) => re.test(raw.trim()));
|
|
19
|
+
if (eigen)
|
|
20
|
+
return raw.trim();
|
|
21
|
+
if (lower.includes("uitgaand floriday-verkeer") || lower.includes("stock_publish")) {
|
|
22
|
+
return `${actie} is nog niet beschikbaar in deze omgeving (de databaseversie is ouder dan de app), neem contact op met de beheerder.`;
|
|
23
|
+
}
|
|
24
|
+
if (code === "42883" || code === "PGRST202" || code === "23514" ||
|
|
25
|
+
lower.includes("could not find the function") || lower.includes("does not exist") ||
|
|
26
|
+
lower.includes("violates check constraint")) {
|
|
27
|
+
return `${actie} is nog niet beschikbaar in deze omgeving, neem contact op met de beheerder.`;
|
|
28
|
+
}
|
|
29
|
+
if (code === "42501" || lower.includes("permission denied") || lower.includes("niet ingelogd")) {
|
|
30
|
+
return "Je hebt geen rechten voor deze actie. Log opnieuw in of vraag de beheerder.";
|
|
31
|
+
}
|
|
32
|
+
if (lower.includes("failed to fetch") || lower.includes("network")) {
|
|
33
|
+
return "Geen verbinding. Controleer je internet en probeer het opnieuw.";
|
|
34
|
+
}
|
|
35
|
+
return `${actie} is niet gelukt. Probeer het opnieuw of neem contact op met de beheerder.`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regels rond het lokaal opslaan van voorraad (1.0.138).
|
|
3
|
+
* - Na een mislukte opslag staan weer de laatst opgeslagen waarden in beeld.
|
|
4
|
+
* - Gereserveerd komt altijd uit de opgeslagen rij (berekend uit orders), nooit uit invoer.
|
|
5
|
+
* - Afboeken vraagt altijd een reden uit een vaste lijst.
|
|
6
|
+
*/
|
|
7
|
+
export interface StockValues {
|
|
8
|
+
physical_stock: number;
|
|
9
|
+
minimum_stock: number;
|
|
10
|
+
reserved_stock: number;
|
|
11
|
+
incoming_stock: number;
|
|
12
|
+
notes: string | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function valuesAfterSave<T extends StockValues>(ok: boolean, entered: T, saved: T): T;
|
|
15
|
+
export declare const AFBOEK_REDENEN: readonly [{
|
|
16
|
+
readonly value: "derving";
|
|
17
|
+
readonly label: "Derving";
|
|
18
|
+
}, {
|
|
19
|
+
readonly value: "kapot";
|
|
20
|
+
readonly label: "Kapot";
|
|
21
|
+
}, {
|
|
22
|
+
readonly value: "eigen_gebruik";
|
|
23
|
+
readonly label: "Eigen gebruik";
|
|
24
|
+
}, {
|
|
25
|
+
readonly value: "overig";
|
|
26
|
+
readonly label: "Overig";
|
|
27
|
+
}];
|
|
28
|
+
export type AfboekReden = (typeof AFBOEK_REDENEN)[number]["value"];
|
|
29
|
+
export declare function afboekFout(aantal: number, reden: string | null, liggend: number): string | null;
|
|
30
|
+
/** Melding na een geslaagde lokale opslag van een Floriday-artikel. */
|
|
31
|
+
export declare function floridayNaOpslaanTekst(isFloridayArtikel: boolean, poortOpen: boolean | null): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* 1.0.140: alleen een bevestiging van de database ({ opgeslagen: true }) telt
|
|
34
|
+
* als opgeslagen. Alles anders (fout, leeg antwoord, teruggedraaid) = mislukt.
|
|
35
|
+
*/
|
|
36
|
+
export declare function opslagBevestigd(result: unknown): boolean;
|
|
37
|
+
export declare const OPSLAAN_MISLUKT_TEKST = "Opslaan mislukt, er is niets gewijzigd";
|
|
38
|
+
/** Melding na opslaan; null-resultaat of fout = mislukt (waarden terugzetten). */
|
|
39
|
+
export declare function opslaanMelding(result: unknown, isFloridayArtikel: boolean): {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
tekst: string;
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=stock-save.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stock-save.d.ts","sourceRoot":"","sources":["../../src/lib/stock-save.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAG3F;AAED,eAAO,MAAM,cAAc;;;;;;;;;;;;EAKjB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC;AAEnE,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/F;AAED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,iBAAiB,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAI3G;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAExD;AAED,eAAO,MAAM,qBAAqB,2CAA2C,CAAC;AAE9E,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,OAAO,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAK1G"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function valuesAfterSave(ok, entered, saved) {
|
|
2
|
+
if (!ok)
|
|
3
|
+
return { ...saved };
|
|
4
|
+
return { ...entered, reserved_stock: saved.reserved_stock };
|
|
5
|
+
}
|
|
6
|
+
export const AFBOEK_REDENEN = [
|
|
7
|
+
{ value: "derving", label: "Derving" },
|
|
8
|
+
{ value: "kapot", label: "Kapot" },
|
|
9
|
+
{ value: "eigen_gebruik", label: "Eigen gebruik" },
|
|
10
|
+
{ value: "overig", label: "Overig" },
|
|
11
|
+
];
|
|
12
|
+
export function afboekFout(aantal, reden, liggend) {
|
|
13
|
+
if (!reden || !AFBOEK_REDENEN.some((r) => r.value === reden))
|
|
14
|
+
return "Kies een reden.";
|
|
15
|
+
if (!Number.isFinite(aantal) || aantal <= 0)
|
|
16
|
+
return "Vul een aantal groter dan 0 in.";
|
|
17
|
+
if (aantal > liggend)
|
|
18
|
+
return `Je kunt niet meer afboeken (${aantal}) dan er ligt (${liggend}).`;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/** Melding na een geslaagde lokale opslag van een Floriday-artikel. */
|
|
22
|
+
export function floridayNaOpslaanTekst(isFloridayArtikel, poortOpen) {
|
|
23
|
+
if (!isFloridayArtikel)
|
|
24
|
+
return null;
|
|
25
|
+
if (poortOpen === true)
|
|
26
|
+
return null;
|
|
27
|
+
return "Lokaal opgeslagen. Er gaat niets naar Floriday: versturen staat uit.";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 1.0.140: alleen een bevestiging van de database ({ opgeslagen: true }) telt
|
|
31
|
+
* als opgeslagen. Alles anders (fout, leeg antwoord, teruggedraaid) = mislukt.
|
|
32
|
+
*/
|
|
33
|
+
export function opslagBevestigd(result) {
|
|
34
|
+
return !!result && typeof result === "object" && result.opgeslagen === true;
|
|
35
|
+
}
|
|
36
|
+
export const OPSLAAN_MISLUKT_TEKST = "Opslaan mislukt, er is niets gewijzigd";
|
|
37
|
+
/** Melding na opslaan; null-resultaat of fout = mislukt (waarden terugzetten). */
|
|
38
|
+
export function opslaanMelding(result, isFloridayArtikel) {
|
|
39
|
+
if (!opslagBevestigd(result))
|
|
40
|
+
return { ok: false, tekst: OPSLAAN_MISLUKT_TEKST };
|
|
41
|
+
const klaar = result.floriday_klaargezet_toegestaan === true;
|
|
42
|
+
if (isFloridayArtikel && !klaar)
|
|
43
|
+
return { ok: true, tekst: "Opgeslagen; er is niets naar Floriday gestuurd." };
|
|
44
|
+
return { ok: true, tekst: "Voorraad opgeslagen" };
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowselections/floriday-voorraad",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.141",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist-lib/index.js",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"preview": "vite preview",
|
|
33
33
|
"lint": "eslint .",
|
|
34
34
|
"test": "vitest run",
|
|
35
|
+
"check:schema": "node scripts/check-schema-version.mjs",
|
|
36
|
+
"prepublishOnly": "node scripts/check-schema-version.mjs",
|
|
35
37
|
"test:watch": "vitest"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
package/sql/AGENTS.md
CHANGED
|
@@ -4,3 +4,6 @@
|
|
|
4
4
|
- Fust rows are resolved only via `voorraad_active_fust_id(key, id)` (follows `merged_into`), and an order line books fust once: the ERP line adopts an earlier Floriday-ingest booking and ingest skips lines the ERP already booked, because `fust_key LIMIT 1` hit merged rows and both paths booked the same order.
|
|
5
5
|
- An order line counts as cancelled for fust when `order_items.quarantined_at` is set or its order is cancelled (`voorraad_order_geannuleerd`: status geannuleerd/cancelled case-insensitive, `cancelled_at` or `deleted_at`): `sync_order_item_fusten` wants 0 and triggers on order_items (quarantine on/off) and orders (those three fields) book the difference against the line's own bookings, because quarantined or cancelled orders otherwise kept fust booked.
|
|
6
6
|
- The correct fust count per order line comes only from `voorraad_fust_juist(order_item_id)` (same rule as `sync_order_item_fusten`; removed, quarantined, externally fulfilled or cancelled = 0); fust corrections compare against it, because every hand-built copy of the rule drifted.
|
|
7
|
+
- Local stock saves never depend on the Floriday write gate: every Voorraad queue insert first checks `voorraad_floriday_klaarzetten_toegestaan()` and runs in its own exception block, because the Engine gate trigger raises and rolled back the local save.
|
|
8
|
+
- A note per stock change travels via tx setting `stock.change_note` (read by the deferred log trigger at commit, so never clear it inside the call); `trade_item_stock.notes` is the fixed article note and changes only when the caller asks (`stock.keep_notes`), because appending each save stacked notes.
|
|
9
|
+
- `sql/floriday-voorraad-schema.sql` must carry the package version and required objects; `scripts/check-schema-version.mjs` (prepublishOnly + test) blocks publishing otherwise, because 1.0.139 shipped with a 1.0.136 schema.
|