@simonarcher/fika-types 2.3.0 → 2.4.0

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.
@@ -42,6 +42,14 @@ export interface BeanScanAiDraft {
42
42
  variety: string | null;
43
43
  roastDate: string | null;
44
44
  weightG: number | null;
45
+ /**
46
+ * FIK-193 — high-level brew-method intent extracted from the bag.
47
+ * One of the four values from `BrewMethodTarget` in
48
+ * `./brewMethodTarget` (literal duplicated to keep the file self-contained).
49
+ * Mobile prefills the AddBean picker from this; admin shows it in the
50
+ * AI vision comparison panel for curator override.
51
+ */
52
+ brewMethodTarget: "espresso" | "filter" | "omni" | "unknown" | null;
45
53
  model: string;
46
54
  latencyMs: number;
47
55
  extractedAt: string;
@@ -65,6 +73,17 @@ export interface BeanScanListItem {
65
73
  ocrText?: string;
66
74
  action: BeanScanAction;
67
75
  matchedBeanId?: string;
76
+ /**
77
+ * Denormalised matched-bean fields, hydrated server-side via a batched
78
+ * coffee_beans + roasters lookup against `matchedBeanId`. Set when the
79
+ * scan resolved to a real catalog bean — i.e. action ∈ {saved, tried}
80
+ * always, and action === 'created_new' once the curator approves the
81
+ * submission. While a created_new submission is still pending, these
82
+ * remain undefined; clients use that to distinguish "Submitted /
83
+ * Pending review" from "Approved / Now in catalog".
84
+ */
85
+ matchedBeanName?: string;
86
+ matchedRoasterName?: string;
68
87
  /**
69
88
  * Denormalised top candidate, derived server-side from the persisted
70
89
  * `candidates` jsonb. Lets the list render a meaningful primary line
@@ -0,0 +1,42 @@
1
+ import type { BrewMethod } from "./coffee";
2
+ /**
3
+ * High-level brew-method intent for a coffee bag (FIK-193).
4
+ *
5
+ * Most specialty bags state how the roaster intends the bean to be brewed:
6
+ * `Espresso`, `Filter`, both ("omni"), or unspecified. The four-value
7
+ * enum below is the user-facing concept; `coffee_beans.brew_recommendations`
8
+ * (BrewMethod[]) is where it lives in the DB. Use the helpers in this file
9
+ * to project between the two so all consumers agree.
10
+ */
11
+ export type BrewMethodTarget = "espresso" | "filter" | "omni" | "unknown";
12
+ export declare const BREW_METHOD_TARGET_VALUES: readonly BrewMethodTarget[];
13
+ /**
14
+ * BrewMethod tokens that count as "filter-style" when projecting an array
15
+ * back to a high-level intent. Includes the canonical `Filter` enum plus
16
+ * pour-over / drip variants users see on bags. Lower-cased on lookup so
17
+ * casing differences in legacy data don't break the projection.
18
+ */
19
+ export declare const FILTER_METHOD_TOKENS: readonly ["Filter", "Pour Over", "Aeropress", "V60", "Kalita", "Chemex", "Clever"];
20
+ export declare const ESPRESSO_METHOD_TOKENS: readonly ["Espresso"];
21
+ /**
22
+ * READ-side projection: BrewMethod[] (or anything roughly array-shaped) →
23
+ * one of the four high-level intents.
24
+ *
25
+ * - has Espresso AND any filter-style token → `omni`
26
+ * - only Espresso → `espresso`
27
+ * - only filter-style → `filter`
28
+ * - empty / null / no recognised tokens → `unknown`
29
+ */
30
+ export declare function brewMethodTargetFromArray(arr: readonly string[] | null | undefined): BrewMethodTarget;
31
+ /**
32
+ * WRITE-side projection: BrewMethodTarget → BrewMethod[] suitable for
33
+ * `coffee_beans.brew_recommendations`. Title Case to match the 96 existing
34
+ * prod rows. `unknown` writes `null` (no array) so we don't flood the column
35
+ * with placeholder data.
36
+ */
37
+ export declare function brewRecommendationsFromTarget(target: BrewMethodTarget | null | undefined): BrewMethod[] | null;
38
+ /**
39
+ * Narrow an arbitrary string to BrewMethodTarget if it's one of the four
40
+ * known values, else null. Useful when reading off the wire / DB.
41
+ */
42
+ export declare function asBrewMethodTarget(v: string | null | undefined): BrewMethodTarget | null;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ESPRESSO_METHOD_TOKENS = exports.FILTER_METHOD_TOKENS = exports.BREW_METHOD_TARGET_VALUES = void 0;
4
+ exports.brewMethodTargetFromArray = brewMethodTargetFromArray;
5
+ exports.brewRecommendationsFromTarget = brewRecommendationsFromTarget;
6
+ exports.asBrewMethodTarget = asBrewMethodTarget;
7
+ exports.BREW_METHOD_TARGET_VALUES = [
8
+ "espresso",
9
+ "filter",
10
+ "omni",
11
+ "unknown",
12
+ ];
13
+ /**
14
+ * BrewMethod tokens that count as "filter-style" when projecting an array
15
+ * back to a high-level intent. Includes the canonical `Filter` enum plus
16
+ * pour-over / drip variants users see on bags. Lower-cased on lookup so
17
+ * casing differences in legacy data don't break the projection.
18
+ */
19
+ exports.FILTER_METHOD_TOKENS = [
20
+ "Filter",
21
+ "Pour Over",
22
+ "Aeropress",
23
+ "V60",
24
+ "Kalita",
25
+ "Chemex",
26
+ "Clever",
27
+ ];
28
+ exports.ESPRESSO_METHOD_TOKENS = ["Espresso"];
29
+ const lc = (s) => s.trim().toLowerCase();
30
+ const FILTER_SET = new Set(exports.FILTER_METHOD_TOKENS.map(lc));
31
+ const ESPRESSO_SET = new Set(exports.ESPRESSO_METHOD_TOKENS.map(lc));
32
+ /**
33
+ * READ-side projection: BrewMethod[] (or anything roughly array-shaped) →
34
+ * one of the four high-level intents.
35
+ *
36
+ * - has Espresso AND any filter-style token → `omni`
37
+ * - only Espresso → `espresso`
38
+ * - only filter-style → `filter`
39
+ * - empty / null / no recognised tokens → `unknown`
40
+ */
41
+ function brewMethodTargetFromArray(arr) {
42
+ if (!arr || arr.length === 0)
43
+ return "unknown";
44
+ let hasEspresso = false;
45
+ let hasFilter = false;
46
+ for (const raw of arr) {
47
+ if (typeof raw !== "string")
48
+ continue;
49
+ const v = lc(raw);
50
+ if (ESPRESSO_SET.has(v))
51
+ hasEspresso = true;
52
+ if (FILTER_SET.has(v))
53
+ hasFilter = true;
54
+ }
55
+ if (hasEspresso && hasFilter)
56
+ return "omni";
57
+ if (hasEspresso)
58
+ return "espresso";
59
+ if (hasFilter)
60
+ return "filter";
61
+ return "unknown";
62
+ }
63
+ /**
64
+ * WRITE-side projection: BrewMethodTarget → BrewMethod[] suitable for
65
+ * `coffee_beans.brew_recommendations`. Title Case to match the 96 existing
66
+ * prod rows. `unknown` writes `null` (no array) so we don't flood the column
67
+ * with placeholder data.
68
+ */
69
+ function brewRecommendationsFromTarget(target) {
70
+ switch (target) {
71
+ case "espresso":
72
+ return ["Espresso"];
73
+ case "filter":
74
+ return ["Filter"];
75
+ case "omni":
76
+ return ["Espresso", "Filter"];
77
+ case "unknown":
78
+ case null:
79
+ case undefined:
80
+ default:
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Narrow an arbitrary string to BrewMethodTarget if it's one of the four
86
+ * known values, else null. Useful when reading off the wire / DB.
87
+ */
88
+ function asBrewMethodTarget(v) {
89
+ if (!v)
90
+ return null;
91
+ const lower = lc(v);
92
+ return exports.BREW_METHOD_TARGET_VALUES.includes(lower)
93
+ ? lower
94
+ : null;
95
+ }
package/dist/coffee.d.ts CHANGED
@@ -196,6 +196,14 @@ export interface SubmissionCoffeeBeanData {
196
196
  tastingNotes?: string[];
197
197
  isDecaf: boolean;
198
198
  decafMethod?: DecafMethod;
199
+ /**
200
+ * FIK-193 — high-level brew-method intent the user picked on AddBean
201
+ * (or that AI vision extracted). Projected to
202
+ * `coffee_beans.brew_recommendations` on curator approval. The literal
203
+ * is duplicated here to keep this file's BrewMethod-adjacent types
204
+ * self-contained; the canonical type lives in `./brewMethodTarget`.
205
+ */
206
+ brewMethodTarget?: "espresso" | "filter" | "omni" | "unknown" | null;
199
207
  userId: string;
200
208
  status: "inReview" | "approved" | "rejected";
201
209
  active: boolean;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './shop';
2
2
  export * from './coffee';
3
+ export * from './brewMethodTarget';
3
4
  export * from './bean-scans';
4
5
  export * from './farm';
5
6
  export * from './user';
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  __exportStar(require("./shop"), exports);
19
19
  // Export all coffee-related types
20
20
  __exportStar(require("./coffee"), exports);
21
+ // Export brew-method-target enum + projection helpers (FIK-193)
22
+ __exportStar(require("./brewMethodTarget"), exports);
21
23
  // Export bag-scan types (design surface; bean_scans table not yet migrated)
22
24
  __exportStar(require("./bean-scans"), exports);
23
25
  // Export all farm-related types
@@ -337,6 +337,7 @@ export type Database = {
337
337
  Row: {
338
338
  active: boolean | null;
339
339
  bean_id: string | null;
340
+ brew_method_target: string | null;
340
341
  country: string | null;
341
342
  created_at: string | null;
342
343
  decaf_method: string | null;
@@ -359,6 +360,7 @@ export type Database = {
359
360
  Insert: {
360
361
  active?: boolean | null;
361
362
  bean_id?: string | null;
363
+ brew_method_target?: string | null;
362
364
  country?: string | null;
363
365
  created_at?: string | null;
364
366
  decaf_method?: string | null;
@@ -381,6 +383,7 @@ export type Database = {
381
383
  Update: {
382
384
  active?: boolean | null;
383
385
  bean_id?: string | null;
386
+ brew_method_target?: string | null;
384
387
  country?: string | null;
385
388
  created_at?: string | null;
386
389
  decaf_method?: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonarcher/fika-types",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Shared TypeScript types for Fika projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",