refactor(products): remove usage notes and contraindications fields
This commit is contained in:
parent
9df241a6a9
commit
013492ec2b
16 changed files with 54 additions and 179 deletions
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""drop_usage_notes_and_contraindications_from_products
|
||||||
|
|
||||||
|
Revision ID: 8e4c1b7a9d2f
|
||||||
|
Revises: f1a2b3c4d5e6
|
||||||
|
Create Date: 2026-03-04 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "8e4c1b7a9d2f"
|
||||||
|
down_revision: Union[str, None] = "f1a2b3c4d5e6"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.drop_column("products", "contraindications")
|
||||||
|
op.drop_column("products", "usage_notes")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"products",
|
||||||
|
sa.Column(
|
||||||
|
"contraindications",
|
||||||
|
sa.JSON(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("'[]'::json"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.alter_column("products", "contraindications", server_default=None)
|
||||||
|
op.add_column("products", sa.Column("usage_notes", sa.String(), nullable=True))
|
||||||
|
|
@ -87,8 +87,6 @@ class ProductUpdate(SQLModel):
|
||||||
recommended_for: Optional[list[SkinType]] = None
|
recommended_for: Optional[list[SkinType]] = None
|
||||||
|
|
||||||
targets: Optional[list[SkinConcern]] = None
|
targets: Optional[list[SkinConcern]] = None
|
||||||
contraindications: Optional[list[str]] = None
|
|
||||||
usage_notes: Optional[str] = None
|
|
||||||
|
|
||||||
fragrance_free: Optional[bool] = None
|
fragrance_free: Optional[bool] = None
|
||||||
essential_oils_free: Optional[bool] = None
|
essential_oils_free: Optional[bool] = None
|
||||||
|
|
@ -139,8 +137,6 @@ class ProductParseResponse(SQLModel):
|
||||||
actives: Optional[list[ActiveIngredient]] = None
|
actives: Optional[list[ActiveIngredient]] = None
|
||||||
recommended_for: Optional[list[SkinType]] = None
|
recommended_for: Optional[list[SkinType]] = None
|
||||||
targets: Optional[list[SkinConcern]] = None
|
targets: Optional[list[SkinConcern]] = None
|
||||||
contraindications: Optional[list[str]] = None
|
|
||||||
usage_notes: Optional[str] = None
|
|
||||||
fragrance_free: Optional[bool] = None
|
fragrance_free: Optional[bool] = None
|
||||||
essential_oils_free: Optional[bool] = None
|
essential_oils_free: Optional[bool] = None
|
||||||
alcohol_denat_free: Optional[bool] = None
|
alcohol_denat_free: Optional[bool] = None
|
||||||
|
|
@ -553,8 +549,6 @@ OUTPUT SCHEMA (all fields optional — omit what you cannot determine):
|
||||||
],
|
],
|
||||||
"recommended_for": [string, ...],
|
"recommended_for": [string, ...],
|
||||||
"targets": [string, ...],
|
"targets": [string, ...],
|
||||||
"contraindications": [string, ...],
|
|
||||||
"usage_notes": string,
|
|
||||||
"fragrance_free": boolean,
|
"fragrance_free": boolean,
|
||||||
"essential_oils_free": boolean,
|
"essential_oils_free": boolean,
|
||||||
"alcohol_denat_free": boolean,
|
"alcohol_denat_free": boolean,
|
||||||
|
|
@ -972,23 +966,12 @@ def _build_actives_tool_handler(products: list[Product]):
|
||||||
return _build_product_details_tool_handler(products, mapper=_mapper)
|
return _build_product_details_tool_handler(products, mapper=_mapper)
|
||||||
|
|
||||||
|
|
||||||
def _build_usage_notes_tool_handler(products: list[Product]):
|
|
||||||
def _mapper(product: Product, pid: str) -> dict[str, object]:
|
|
||||||
notes = " ".join(str(product.usage_notes or "").split())
|
|
||||||
if len(notes) > 500:
|
|
||||||
notes = notes[:497] + "..."
|
|
||||||
return {"id": pid, "name": product.name, "usage_notes": notes}
|
|
||||||
|
|
||||||
return _build_product_details_tool_handler(products, mapper=_mapper)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_safety_rules_tool_handler(products: list[Product]):
|
def _build_safety_rules_tool_handler(products: list[Product]):
|
||||||
def _mapper(product: Product, pid: str) -> dict[str, object]:
|
def _mapper(product: Product, pid: str) -> dict[str, object]:
|
||||||
ctx = product.to_llm_context()
|
ctx = product.to_llm_context()
|
||||||
return {
|
return {
|
||||||
"id": pid,
|
"id": pid,
|
||||||
"name": product.name,
|
"name": product.name,
|
||||||
"contraindications": (ctx.get("contraindications") or [])[:24],
|
|
||||||
"context_rules": ctx.get("context_rules") or {},
|
"context_rules": ctx.get("context_rules") or {},
|
||||||
"safety": ctx.get("safety") or {},
|
"safety": ctx.get("safety") or {},
|
||||||
"min_interval_hours": ctx.get("min_interval_hours"),
|
"min_interval_hours": ctx.get("min_interval_hours"),
|
||||||
|
|
@ -1020,8 +1003,12 @@ _INCI_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
|
||||||
_SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
|
_SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
|
||||||
name="get_product_safety_rules",
|
name="get_product_safety_rules",
|
||||||
description=(
|
description=(
|
||||||
"Return safety and compatibility rules for selected product UUIDs, "
|
"Return structured safety metadata for selected product UUIDs. "
|
||||||
"including contraindications, context_rules and safety flags."
|
"context_rules explain when a product should be avoided in a routine "
|
||||||
|
"(for example after acids, after retinoids, or with a compromised barrier). "
|
||||||
|
"safety flags describe formulation-level constraints "
|
||||||
|
"(for example whether the formula is fragrance-free, whether it contains denatured alcohol, and whether it is pregnancy-safe). "
|
||||||
|
"Also includes min_interval_hours and max_frequency_per_week."
|
||||||
),
|
),
|
||||||
parameters=genai_types.Schema(
|
parameters=genai_types.Schema(
|
||||||
type=genai_types.Type.OBJECT,
|
type=genai_types.Type.OBJECT,
|
||||||
|
|
@ -1055,26 +1042,6 @@ _ACTIVES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
_USAGE_NOTES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
|
|
||||||
name="get_product_usage_notes",
|
|
||||||
description=(
|
|
||||||
"Return compact usage notes for selected product UUIDs "
|
|
||||||
"(timing, application method and cautions)."
|
|
||||||
),
|
|
||||||
parameters=genai_types.Schema(
|
|
||||||
type=genai_types.Type.OBJECT,
|
|
||||||
properties={
|
|
||||||
"product_ids": genai_types.Schema(
|
|
||||||
type=genai_types.Type.ARRAY,
|
|
||||||
items=genai_types.Schema(type=genai_types.Type.STRING),
|
|
||||||
description="Product UUIDs from POSIADANE PRODUKTY.",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
required=["product_ids"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_SHOPPING_SYSTEM_PROMPT = """Jesteś asystentem zakupowym w dziedzinie pielęgnacji skóry.
|
_SHOPPING_SYSTEM_PROMPT = """Jesteś asystentem zakupowym w dziedzinie pielęgnacji skóry.
|
||||||
Twoim zadaniem jest przeanalizować stan skóry użytkownika oraz produkty, które już posiada,
|
Twoim zadaniem jest przeanalizować stan skóry użytkownika oraz produkty, które już posiada,
|
||||||
a następnie zasugerować TYPY produktów (bez marek), które mogłyby uzupełnić ich rutynę.
|
a następnie zasugerować TYPY produktów (bez marek), które mogłyby uzupełnić ich rutynę.
|
||||||
|
|
@ -1108,7 +1075,7 @@ def suggest_shopping(session: Session = Depends(get_session)):
|
||||||
f"mogłyby uzupełnić rutynę pielęgnacyjną użytkownika.\n\n"
|
f"mogłyby uzupełnić rutynę pielęgnacyjną użytkownika.\n\n"
|
||||||
f"{context}\n\n"
|
f"{context}\n\n"
|
||||||
"NARZEDZIA:\n"
|
"NARZEDZIA:\n"
|
||||||
"- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives, get_product_usage_notes.\n"
|
"- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives.\n"
|
||||||
"- Wywoluj narzedzia tylko, gdy potrzebujesz detali do oceny konfliktow skladnikow lub ryzyka podraznien.\n"
|
"- Wywoluj narzedzia tylko, gdy potrzebujesz detali do oceny konfliktow skladnikow lub ryzyka podraznien.\n"
|
||||||
"- Grupuj UUID: staraj sie pobierac dane dla wielu produktow jednym wywolaniem.\n"
|
"- Grupuj UUID: staraj sie pobierac dane dla wielu produktow jednym wywolaniem.\n"
|
||||||
f"Zwróć wyłącznie JSON zgodny ze schematem."
|
f"Zwróć wyłącznie JSON zgodny ze schematem."
|
||||||
|
|
@ -1126,7 +1093,6 @@ def suggest_shopping(session: Session = Depends(get_session)):
|
||||||
_INCI_FUNCTION_DECLARATION,
|
_INCI_FUNCTION_DECLARATION,
|
||||||
_SAFETY_RULES_FUNCTION_DECLARATION,
|
_SAFETY_RULES_FUNCTION_DECLARATION,
|
||||||
_ACTIVES_FUNCTION_DECLARATION,
|
_ACTIVES_FUNCTION_DECLARATION,
|
||||||
_USAGE_NOTES_FUNCTION_DECLARATION,
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
@ -1142,7 +1108,6 @@ def suggest_shopping(session: Session = Depends(get_session)):
|
||||||
"get_product_inci": _build_inci_tool_handler(shopping_products),
|
"get_product_inci": _build_inci_tool_handler(shopping_products),
|
||||||
"get_product_safety_rules": _build_safety_rules_tool_handler(shopping_products),
|
"get_product_safety_rules": _build_safety_rules_tool_handler(shopping_products),
|
||||||
"get_product_actives": _build_actives_tool_handler(shopping_products),
|
"get_product_actives": _build_actives_tool_handler(shopping_products),
|
||||||
"get_product_usage_notes": _build_usage_notes_tool_handler(shopping_products),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -201,8 +201,6 @@ def _is_minoxidil_product(product: Product) -> bool:
|
||||||
return True
|
return True
|
||||||
if _contains_minoxidil_text(product.line_name):
|
if _contains_minoxidil_text(product.line_name):
|
||||||
return True
|
return True
|
||||||
if _contains_minoxidil_text(product.usage_notes):
|
|
||||||
return True
|
|
||||||
if any(_contains_minoxidil_text(i) for i in (product.inci or [])):
|
if any(_contains_minoxidil_text(i) for i in (product.inci or [])):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -378,8 +376,6 @@ def _build_products_context(
|
||||||
notable = {k: v for k, v in profile.items() if v and v > 0}
|
notable = {k: v for k, v in profile.items() if v and v > 0}
|
||||||
if notable:
|
if notable:
|
||||||
entry += f" effects={notable}"
|
entry += f" effects={notable}"
|
||||||
if ctx.get("contraindications"):
|
|
||||||
entry += f" contraindications={ctx['contraindications']}"
|
|
||||||
if ctx.get("context_rules"):
|
if ctx.get("context_rules"):
|
||||||
entry += f" context_rules={ctx['context_rules']}"
|
entry += f" context_rules={ctx['context_rules']}"
|
||||||
safety = ctx.get("safety") or {}
|
safety = ctx.get("safety") or {}
|
||||||
|
|
@ -475,22 +471,6 @@ def _build_actives_tool_handler(
|
||||||
return _build_product_details_tool_handler(products, mapper=_mapper)
|
return _build_product_details_tool_handler(products, mapper=_mapper)
|
||||||
|
|
||||||
|
|
||||||
def _build_usage_notes_tool_handler(
|
|
||||||
products: list[Product],
|
|
||||||
):
|
|
||||||
def _mapper(product: Product, pid: str) -> dict[str, object]:
|
|
||||||
notes = " ".join(str(product.usage_notes or "").split())
|
|
||||||
if len(notes) > 500:
|
|
||||||
notes = notes[:497] + "..."
|
|
||||||
return {
|
|
||||||
"id": pid,
|
|
||||||
"name": product.name,
|
|
||||||
"usage_notes": notes,
|
|
||||||
}
|
|
||||||
|
|
||||||
return _build_product_details_tool_handler(products, mapper=_mapper)
|
|
||||||
|
|
||||||
|
|
||||||
def _build_safety_rules_tool_handler(
|
def _build_safety_rules_tool_handler(
|
||||||
products: list[Product],
|
products: list[Product],
|
||||||
):
|
):
|
||||||
|
|
@ -499,7 +479,6 @@ def _build_safety_rules_tool_handler(
|
||||||
return {
|
return {
|
||||||
"id": pid,
|
"id": pid,
|
||||||
"name": product.name,
|
"name": product.name,
|
||||||
"contraindications": (ctx.get("contraindications") or [])[:24],
|
|
||||||
"context_rules": ctx.get("context_rules") or {},
|
"context_rules": ctx.get("context_rules") or {},
|
||||||
"safety": ctx.get("safety") or {},
|
"safety": ctx.get("safety") or {},
|
||||||
"min_interval_hours": ctx.get("min_interval_hours"),
|
"min_interval_hours": ctx.get("min_interval_hours"),
|
||||||
|
|
@ -604,30 +583,15 @@ _ACTIVES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
_USAGE_NOTES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
|
|
||||||
name="get_product_usage_notes",
|
|
||||||
description=(
|
|
||||||
"Return compact usage notes for selected product UUIDs (application method, "
|
|
||||||
"timing, and cautions)."
|
|
||||||
),
|
|
||||||
parameters=genai_types.Schema(
|
|
||||||
type=genai_types.Type.OBJECT,
|
|
||||||
properties={
|
|
||||||
"product_ids": genai_types.Schema(
|
|
||||||
type=genai_types.Type.ARRAY,
|
|
||||||
items=genai_types.Schema(type=genai_types.Type.STRING),
|
|
||||||
description="Product UUIDs from AVAILABLE PRODUCTS.",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
required=["product_ids"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
_SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
|
_SAFETY_RULES_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
|
||||||
name="get_product_safety_rules",
|
name="get_product_safety_rules",
|
||||||
description=(
|
description=(
|
||||||
"Return safety and compatibility rules for selected product UUIDs: "
|
"Return structured safety metadata for selected product UUIDs. "
|
||||||
"contraindications, context_rules and safety flags."
|
"context_rules explain when a product should be avoided in a routine "
|
||||||
|
"(for example after acids, after retinoids, or with a compromised barrier). "
|
||||||
|
"safety flags describe formulation-level constraints "
|
||||||
|
"(for example whether the formula is fragrance-free, whether it contains denatured alcohol, and whether it is pregnancy-safe). "
|
||||||
|
"Also includes min_interval_hours and max_frequency_per_week."
|
||||||
),
|
),
|
||||||
parameters=genai_types.Schema(
|
parameters=genai_types.Schema(
|
||||||
type=genai_types.Type.OBJECT,
|
type=genai_types.Type.OBJECT,
|
||||||
|
|
@ -685,7 +649,7 @@ WYMAGANIA ODPOWIEDZI:
|
||||||
|
|
||||||
ZASADY PLANOWANIA:
|
ZASADY PLANOWANIA:
|
||||||
- Kolejność warstw: cleanser -> toner -> essence -> serum -> moisturizer -> [SPF dla AM].
|
- Kolejność warstw: cleanser -> toner -> essence -> serum -> moisturizer -> [SPF dla AM].
|
||||||
- Respektuj: context_rules, min_interval_hours, max_frequency_per_week, usage_notes.
|
- Respektuj: context_rules, min_interval_hours, max_frequency_per_week.
|
||||||
- Zarządzanie inwentarzem:
|
- Zarządzanie inwentarzem:
|
||||||
- Preferuj produkty już otwarte (miękka preferencja).
|
- Preferuj produkty już otwarte (miękka preferencja).
|
||||||
- Unikaj funkcjonalnej redundancji (np. wielokrotne źródła panthenolu, ceramidów lub niacynamidu w tej samej rutynie),
|
- Unikaj funkcjonalnej redundancji (np. wielokrotne źródła panthenolu, ceramidów lub niacynamidu w tej samej rutynie),
|
||||||
|
|
@ -821,7 +785,7 @@ def suggest_routine(
|
||||||
"INPUT DATA:\n"
|
"INPUT DATA:\n"
|
||||||
f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{day_ctx}\n{products_ctx}\n{objectives_ctx}"
|
f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{day_ctx}\n{products_ctx}\n{objectives_ctx}"
|
||||||
"\nNARZEDZIA:\n"
|
"\nNARZEDZIA:\n"
|
||||||
"- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives, get_product_usage_notes.\n"
|
"- Masz dostep do funkcji: get_product_inci, get_product_safety_rules, get_product_actives.\n"
|
||||||
"- Wywoluj narzedzia tylko, gdy potrzebujesz detali do decyzji klinicznej/bezpieczenstwa.\n"
|
"- Wywoluj narzedzia tylko, gdy potrzebujesz detali do decyzji klinicznej/bezpieczenstwa.\n"
|
||||||
"- Staraj sie grupowac zapytania: podawaj wszystkie potrzebne UUID w jednym wywolaniu narzedzia.\n"
|
"- Staraj sie grupowac zapytania: podawaj wszystkie potrzebne UUID w jednym wywolaniu narzedzia.\n"
|
||||||
"- Nie zgaduj detali skladu i zasad bezpieczenstwa; jesli potrzebujesz szczegolow, wywolaj odpowiednie narzedzie.\n"
|
"- Nie zgaduj detali skladu i zasad bezpieczenstwa; jesli potrzebujesz szczegolow, wywolaj odpowiednie narzedzie.\n"
|
||||||
|
|
@ -842,7 +806,6 @@ def suggest_routine(
|
||||||
_INCI_FUNCTION_DECLARATION,
|
_INCI_FUNCTION_DECLARATION,
|
||||||
_SAFETY_RULES_FUNCTION_DECLARATION,
|
_SAFETY_RULES_FUNCTION_DECLARATION,
|
||||||
_ACTIVES_FUNCTION_DECLARATION,
|
_ACTIVES_FUNCTION_DECLARATION,
|
||||||
_USAGE_NOTES_FUNCTION_DECLARATION,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
@ -860,7 +823,6 @@ def suggest_routine(
|
||||||
available_products
|
available_products
|
||||||
),
|
),
|
||||||
"get_product_actives": _build_actives_tool_handler(available_products),
|
"get_product_actives": _build_actives_tool_handler(available_products),
|
||||||
"get_product_usage_notes": _build_usage_notes_tool_handler(available_products),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,6 @@ class ProductBase(SQLModel):
|
||||||
recommended_for: list[SkinType] = Field(default_factory=list)
|
recommended_for: list[SkinType] = Field(default_factory=list)
|
||||||
|
|
||||||
targets: list[SkinConcern] = Field(default_factory=list)
|
targets: list[SkinConcern] = Field(default_factory=list)
|
||||||
contraindications: list[str] = Field(default_factory=list)
|
|
||||||
usage_notes: str | None = None
|
|
||||||
|
|
||||||
fragrance_free: bool | None = None
|
fragrance_free: bool | None = None
|
||||||
essential_oils_free: bool | None = None
|
essential_oils_free: bool | None = None
|
||||||
alcohol_denat_free: bool | None = None
|
alcohol_denat_free: bool | None = None
|
||||||
|
|
@ -161,9 +158,6 @@ class Product(ProductBase, table=True):
|
||||||
targets: list[SkinConcern] = Field(
|
targets: list[SkinConcern] = Field(
|
||||||
default_factory=list, sa_column=Column(JSON, nullable=False)
|
default_factory=list, sa_column=Column(JSON, nullable=False)
|
||||||
)
|
)
|
||||||
contraindications: list[str] = Field(
|
|
||||||
default_factory=list, sa_column=Column(JSON, nullable=False)
|
|
||||||
)
|
|
||||||
|
|
||||||
product_effect_profile: ProductEffectProfile = Field(
|
product_effect_profile: ProductEffectProfile = Field(
|
||||||
default_factory=ProductEffectProfile,
|
default_factory=ProductEffectProfile,
|
||||||
|
|
@ -217,9 +211,6 @@ class Product(ProductBase, table=True):
|
||||||
if self.category == ProductCategory.SPF and not self.leave_on:
|
if self.category == ProductCategory.SPF and not self.leave_on:
|
||||||
raise ValueError("SPF products must be leave-on")
|
raise ValueError("SPF products must be leave-on")
|
||||||
|
|
||||||
if self.is_medication and not self.usage_notes:
|
|
||||||
raise ValueError("Medication products must have usage_notes")
|
|
||||||
|
|
||||||
if self.price_currency is not None:
|
if self.price_currency is not None:
|
||||||
self.price_currency = self.price_currency.upper()
|
self.price_currency = self.price_currency.upper()
|
||||||
|
|
||||||
|
|
@ -267,9 +258,6 @@ class Product(ProductBase, table=True):
|
||||||
ctx["recommended_for"] = [_ev(s) for s in self.recommended_for]
|
ctx["recommended_for"] = [_ev(s) for s in self.recommended_for]
|
||||||
if self.targets:
|
if self.targets:
|
||||||
ctx["targets"] = [_ev(s) for s in self.targets]
|
ctx["targets"] = [_ev(s) for s in self.targets]
|
||||||
if self.contraindications:
|
|
||||||
ctx["contraindications"] = self.contraindications
|
|
||||||
|
|
||||||
if self.actives:
|
if self.actives:
|
||||||
actives_ctx = []
|
actives_ctx = []
|
||||||
for a in self.actives:
|
for a in self.actives:
|
||||||
|
|
@ -337,9 +325,6 @@ class Product(ProductBase, table=True):
|
||||||
ctx["is_tool"] = True
|
ctx["is_tool"] = True
|
||||||
if self.needle_length_mm is not None:
|
if self.needle_length_mm is not None:
|
||||||
ctx["needle_length_mm"] = self.needle_length_mm
|
ctx["needle_length_mm"] = self.needle_length_mm
|
||||||
if self.usage_notes:
|
|
||||||
ctx["usage_notes"] = self.usage_notes
|
|
||||||
|
|
||||||
if self.personal_tolerance_notes:
|
if self.personal_tolerance_notes:
|
||||||
ctx["personal_tolerance_notes"] = self.personal_tolerance_notes
|
ctx["personal_tolerance_notes"] = self.personal_tolerance_notes
|
||||||
if self.personal_repurchase_intent is not None:
|
if self.personal_repurchase_intent is not None:
|
||||||
|
|
|
||||||
|
|
@ -95,14 +95,12 @@ def test_list_filter_is_medication(client):
|
||||||
"category": "serum",
|
"category": "serum",
|
||||||
}
|
}
|
||||||
client.post("/products", json={**base, "name": "Normal", "is_medication": False})
|
client.post("/products", json={**base, "name": "Normal", "is_medication": False})
|
||||||
# is_medication=True requires usage_notes (model validator)
|
|
||||||
client.post(
|
client.post(
|
||||||
"/products",
|
"/products",
|
||||||
json={
|
json={
|
||||||
**base,
|
**base,
|
||||||
"name": "Med",
|
"name": "Med",
|
||||||
"is_medication": True,
|
"is_medication": True,
|
||||||
"usage_notes": "Apply pea-sized amount",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ from innercontext.api.products import (
|
||||||
_build_inci_tool_handler,
|
_build_inci_tool_handler,
|
||||||
_build_safety_rules_tool_handler,
|
_build_safety_rules_tool_handler,
|
||||||
_build_shopping_context,
|
_build_shopping_context,
|
||||||
_build_usage_notes_tool_handler,
|
|
||||||
_extract_requested_product_ids,
|
_extract_requested_product_ids,
|
||||||
)
|
)
|
||||||
from innercontext.models import Product, ProductInventory, SkinConditionSnapshot
|
from innercontext.models import Product, ProductInventory, SkinConditionSnapshot
|
||||||
|
|
@ -96,7 +95,6 @@ def test_suggest_shopping(client, session):
|
||||||
assert "get_product_inci" in kwargs["function_handlers"]
|
assert "get_product_inci" in kwargs["function_handlers"]
|
||||||
assert "get_product_safety_rules" in kwargs["function_handlers"]
|
assert "get_product_safety_rules" in kwargs["function_handlers"]
|
||||||
assert "get_product_actives" in kwargs["function_handlers"]
|
assert "get_product_actives" in kwargs["function_handlers"]
|
||||||
assert "get_product_usage_notes" in kwargs["function_handlers"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_shopping_context_medication_skip(session: Session):
|
def test_shopping_context_medication_skip(session: Session):
|
||||||
|
|
@ -133,7 +131,6 @@ def test_shopping_tool_handlers_return_payloads(session: Session):
|
||||||
category="serum",
|
category="serum",
|
||||||
recommended_time="both",
|
recommended_time="both",
|
||||||
leave_on=True,
|
leave_on=True,
|
||||||
usage_notes="Use AM and PM on clean skin.",
|
|
||||||
inci=["Water", "Niacinamide"],
|
inci=["Water", "Niacinamide"],
|
||||||
actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}],
|
actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}],
|
||||||
context_rules={"safe_after_shaving": True},
|
context_rules={"safe_after_shaving": True},
|
||||||
|
|
@ -148,8 +145,5 @@ def test_shopping_tool_handlers_return_payloads(session: Session):
|
||||||
actives_data = _build_actives_tool_handler([product])(payload)
|
actives_data = _build_actives_tool_handler([product])(payload)
|
||||||
assert actives_data["products"][0]["actives"][0]["name"] == "Niacinamide"
|
assert actives_data["products"][0]["actives"][0]["name"] == "Niacinamide"
|
||||||
|
|
||||||
notes_data = _build_usage_notes_tool_handler([product])(payload)
|
|
||||||
assert notes_data["products"][0]["usage_notes"] == "Use AM and PM on clean skin."
|
|
||||||
|
|
||||||
safety_data = _build_safety_rules_tool_handler([product])(payload)
|
safety_data = _build_safety_rules_tool_handler([product])(payload)
|
||||||
assert "context_rules" in safety_data["products"][0]
|
assert "context_rules" in safety_data["products"][0]
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,6 @@ def test_suggest_routine(client, session):
|
||||||
assert "get_product_inci" in kwargs["function_handlers"]
|
assert "get_product_inci" in kwargs["function_handlers"]
|
||||||
assert "get_product_safety_rules" in kwargs["function_handlers"]
|
assert "get_product_safety_rules" in kwargs["function_handlers"]
|
||||||
assert "get_product_actives" in kwargs["function_handlers"]
|
assert "get_product_actives" in kwargs["function_handlers"]
|
||||||
assert "get_product_usage_notes" in kwargs["function_handlers"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_suggest_batch(client, session):
|
def test_suggest_batch(client, session):
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ from innercontext.api.routines import (
|
||||||
_build_recent_history,
|
_build_recent_history,
|
||||||
_build_safety_rules_tool_handler,
|
_build_safety_rules_tool_handler,
|
||||||
_build_skin_context,
|
_build_skin_context,
|
||||||
_build_usage_notes_tool_handler,
|
|
||||||
_contains_minoxidil_text,
|
_contains_minoxidil_text,
|
||||||
_ev,
|
_ev,
|
||||||
_extract_active_names,
|
_extract_active_names,
|
||||||
|
|
@ -56,10 +55,6 @@ def test_is_minoxidil_product():
|
||||||
assert _is_minoxidil_product(p) is True
|
assert _is_minoxidil_product(p) is True
|
||||||
|
|
||||||
p.line_name = None
|
p.line_name = None
|
||||||
p.usage_notes = "Use minoxidil daily"
|
|
||||||
assert _is_minoxidil_product(p) is True
|
|
||||||
|
|
||||||
p.usage_notes = None
|
|
||||||
p.inci = ["water", "minoxidil"]
|
p.inci = ["water", "minoxidil"]
|
||||||
assert _is_minoxidil_product(p) is True
|
assert _is_minoxidil_product(p) is True
|
||||||
|
|
||||||
|
|
@ -386,7 +381,6 @@ def test_additional_tool_handlers_return_product_payloads(session: Session):
|
||||||
brand="Test",
|
brand="Test",
|
||||||
recommended_time="both",
|
recommended_time="both",
|
||||||
leave_on=True,
|
leave_on=True,
|
||||||
usage_notes="Apply morning and evening.",
|
|
||||||
actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}],
|
actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}],
|
||||||
context_rules={"safe_after_shaving": True},
|
context_rules={"safe_after_shaving": True},
|
||||||
product_effect_profile={},
|
product_effect_profile={},
|
||||||
|
|
@ -397,8 +391,5 @@ def test_additional_tool_handlers_return_product_payloads(session: Session):
|
||||||
actives_out = _build_actives_tool_handler([p])(ids_payload)
|
actives_out = _build_actives_tool_handler([p])(ids_payload)
|
||||||
assert actives_out["products"][0]["actives"][0]["name"] == "Niacinamide"
|
assert actives_out["products"][0]["actives"][0]["name"] == "Niacinamide"
|
||||||
|
|
||||||
notes_out = _build_usage_notes_tool_handler([p])(ids_payload)
|
|
||||||
assert notes_out["products"][0]["usage_notes"] == "Apply morning and evening."
|
|
||||||
|
|
||||||
safety_out = _build_safety_rules_tool_handler([p])(ids_payload)
|
safety_out = _build_safety_rules_tool_handler([p])(ids_payload)
|
||||||
assert "context_rules" in safety_out["products"][0]
|
assert "context_rules" in safety_out["products"][0]
|
||||||
|
|
|
||||||
|
|
@ -380,8 +380,6 @@
|
||||||
"productForm_skinProfile": "Skin profile",
|
"productForm_skinProfile": "Skin profile",
|
||||||
"productForm_recommendedFor": "Recommended for skin types",
|
"productForm_recommendedFor": "Recommended for skin types",
|
||||||
"productForm_targetConcerns": "Target concerns",
|
"productForm_targetConcerns": "Target concerns",
|
||||||
"productForm_contraindications": "Contraindications (one per line)",
|
|
||||||
"productForm_contraindicationsPlaceholder": "e.g. active rosacea flares",
|
|
||||||
"productForm_ingredients": "Ingredients",
|
"productForm_ingredients": "Ingredients",
|
||||||
"productForm_inciList": "INCI list (one ingredient per line)",
|
"productForm_inciList": "INCI list (one ingredient per line)",
|
||||||
"productForm_inciPlaceholder": "Aqua\nGlycerin\nNiacinamide",
|
"productForm_inciPlaceholder": "Aqua\nGlycerin\nNiacinamide",
|
||||||
|
|
|
||||||
|
|
@ -394,8 +394,6 @@
|
||||||
"productForm_skinProfile": "Profil skóry",
|
"productForm_skinProfile": "Profil skóry",
|
||||||
"productForm_recommendedFor": "Polecane dla typów skóry",
|
"productForm_recommendedFor": "Polecane dla typów skóry",
|
||||||
"productForm_targetConcerns": "Problemy docelowe",
|
"productForm_targetConcerns": "Problemy docelowe",
|
||||||
"productForm_contraindications": "Przeciwwskazania (jedno na linię)",
|
|
||||||
"productForm_contraindicationsPlaceholder": "np. aktywna rosacea",
|
|
||||||
"productForm_ingredients": "Składniki",
|
"productForm_ingredients": "Składniki",
|
||||||
"productForm_inciList": "Lista INCI (jeden składnik na linię)",
|
"productForm_inciList": "Lista INCI (jeden składnik na linię)",
|
||||||
"productForm_inciPlaceholder": "Aqua\nGlycerin\nNiacinamide",
|
"productForm_inciPlaceholder": "Aqua\nGlycerin\nNiacinamide",
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,6 @@ export interface ProductParseResponse {
|
||||||
actives?: ActiveIngredient[];
|
actives?: ActiveIngredient[];
|
||||||
recommended_for?: string[];
|
recommended_for?: string[];
|
||||||
targets?: string[];
|
targets?: string[];
|
||||||
contraindications?: string[];
|
|
||||||
usage_notes?: string;
|
|
||||||
fragrance_free?: boolean;
|
fragrance_free?: boolean;
|
||||||
essential_oils_free?: boolean;
|
essential_oils_free?: boolean;
|
||||||
alcohol_denat_free?: boolean;
|
alcohol_denat_free?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -173,9 +173,7 @@
|
||||||
let minIntervalHours = $state(untrack(() => (product?.min_interval_hours != null ? String(product.min_interval_hours) : '')));
|
let minIntervalHours = $state(untrack(() => (product?.min_interval_hours != null ? String(product.min_interval_hours) : '')));
|
||||||
let maxFrequencyPerWeek = $state(untrack(() => (product?.max_frequency_per_week != null ? String(product.max_frequency_per_week) : '')));
|
let maxFrequencyPerWeek = $state(untrack(() => (product?.max_frequency_per_week != null ? String(product.max_frequency_per_week) : '')));
|
||||||
let needleLengthMm = $state(untrack(() => (product?.needle_length_mm != null ? String(product.needle_length_mm) : '')));
|
let needleLengthMm = $state(untrack(() => (product?.needle_length_mm != null ? String(product.needle_length_mm) : '')));
|
||||||
let usageNotes = $state(untrack(() => product?.usage_notes ?? ''));
|
|
||||||
let inciText = $state(untrack(() => product?.inci?.join('\n') ?? ''));
|
let inciText = $state(untrack(() => product?.inci?.join('\n') ?? ''));
|
||||||
let contraindicationsText = $state(untrack(() => product?.contraindications?.join('\n') ?? ''));
|
|
||||||
let personalToleranceNotes = $state(untrack(() => product?.personal_tolerance_notes ?? ''));
|
let personalToleranceNotes = $state(untrack(() => product?.personal_tolerance_notes ?? ''));
|
||||||
|
|
||||||
let recommendedFor = $state<string[]>(untrack(() => [...(product?.recommended_for ?? [])]));
|
let recommendedFor = $state<string[]>(untrack(() => [...(product?.recommended_for ?? [])]));
|
||||||
|
|
@ -215,7 +213,6 @@
|
||||||
if (r.url) url = r.url;
|
if (r.url) url = r.url;
|
||||||
if (r.sku) sku = r.sku;
|
if (r.sku) sku = r.sku;
|
||||||
if (r.barcode) barcode = r.barcode;
|
if (r.barcode) barcode = r.barcode;
|
||||||
if (r.usage_notes) usageNotes = r.usage_notes;
|
|
||||||
if (r.category) category = r.category;
|
if (r.category) category = r.category;
|
||||||
if (r.recommended_time) recommendedTime = r.recommended_time;
|
if (r.recommended_time) recommendedTime = r.recommended_time;
|
||||||
if (r.texture) texture = r.texture;
|
if (r.texture) texture = r.texture;
|
||||||
|
|
@ -241,7 +238,6 @@
|
||||||
if (r.is_medication != null) isMedication = r.is_medication;
|
if (r.is_medication != null) isMedication = r.is_medication;
|
||||||
if (r.is_tool != null) isTool = r.is_tool;
|
if (r.is_tool != null) isTool = r.is_tool;
|
||||||
if (r.inci?.length) inciText = r.inci.join('\n');
|
if (r.inci?.length) inciText = r.inci.join('\n');
|
||||||
if (r.contraindications?.length) contraindicationsText = r.contraindications.join('\n');
|
|
||||||
if (r.actives?.length) {
|
if (r.actives?.length) {
|
||||||
actives = r.actives.map((a) => ({
|
actives = r.actives.map((a) => ({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
|
|
@ -415,9 +411,7 @@
|
||||||
minIntervalHours,
|
minIntervalHours,
|
||||||
maxFrequencyPerWeek,
|
maxFrequencyPerWeek,
|
||||||
needleLengthMm,
|
needleLengthMm,
|
||||||
usageNotes,
|
|
||||||
inciText,
|
inciText,
|
||||||
contraindicationsText,
|
|
||||||
personalToleranceNotes,
|
personalToleranceNotes,
|
||||||
recommendedFor,
|
recommendedFor,
|
||||||
targetConcerns,
|
targetConcerns,
|
||||||
|
|
@ -589,17 +583,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="contraindications">{m["productForm_contraindications"]()}</Label>
|
|
||||||
<textarea
|
|
||||||
id="contraindications"
|
|
||||||
name="contraindications"
|
|
||||||
rows="2"
|
|
||||||
placeholder={m["productForm_contraindicationsPlaceholder"]()}
|
|
||||||
class={textareaClass}
|
|
||||||
bind:value={contraindicationsText}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -737,7 +720,6 @@
|
||||||
{@const DetailsSection = mod.default}
|
{@const DetailsSection = mod.default}
|
||||||
<DetailsSection
|
<DetailsSection
|
||||||
visible={editSection === 'details'}
|
visible={editSection === 'details'}
|
||||||
{textareaClass}
|
|
||||||
bind:priceAmount
|
bind:priceAmount
|
||||||
bind:priceCurrency
|
bind:priceCurrency
|
||||||
bind:sizeMl
|
bind:sizeMl
|
||||||
|
|
@ -746,7 +728,6 @@
|
||||||
bind:paoMonths
|
bind:paoMonths
|
||||||
bind:phMin
|
bind:phMin
|
||||||
bind:phMax
|
bind:phMax
|
||||||
bind:usageNotes
|
|
||||||
bind:minIntervalHours
|
bind:minIntervalHours
|
||||||
bind:maxFrequencyPerWeek
|
bind:maxFrequencyPerWeek
|
||||||
bind:needleLengthMm
|
bind:needleLengthMm
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
let {
|
let {
|
||||||
visible = false,
|
visible = false,
|
||||||
textareaClass,
|
|
||||||
priceAmount = $bindable(''),
|
priceAmount = $bindable(''),
|
||||||
priceCurrency = $bindable('PLN'),
|
priceCurrency = $bindable('PLN'),
|
||||||
sizeMl = $bindable(''),
|
sizeMl = $bindable(''),
|
||||||
|
|
@ -15,7 +14,6 @@
|
||||||
paoMonths = $bindable(''),
|
paoMonths = $bindable(''),
|
||||||
phMin = $bindable(''),
|
phMin = $bindable(''),
|
||||||
phMax = $bindable(''),
|
phMax = $bindable(''),
|
||||||
usageNotes = $bindable(''),
|
|
||||||
minIntervalHours = $bindable(''),
|
minIntervalHours = $bindable(''),
|
||||||
maxFrequencyPerWeek = $bindable(''),
|
maxFrequencyPerWeek = $bindable(''),
|
||||||
needleLengthMm = $bindable(''),
|
needleLengthMm = $bindable(''),
|
||||||
|
|
@ -26,7 +24,6 @@
|
||||||
computedPriceTierLabel
|
computedPriceTierLabel
|
||||||
}: {
|
}: {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
textareaClass: string;
|
|
||||||
priceAmount?: string;
|
priceAmount?: string;
|
||||||
priceCurrency?: string;
|
priceCurrency?: string;
|
||||||
sizeMl?: string;
|
sizeMl?: string;
|
||||||
|
|
@ -35,7 +32,6 @@
|
||||||
paoMonths?: string;
|
paoMonths?: string;
|
||||||
phMin?: string;
|
phMin?: string;
|
||||||
phMax?: string;
|
phMax?: string;
|
||||||
usageNotes?: string;
|
|
||||||
minIntervalHours?: string;
|
minIntervalHours?: string;
|
||||||
maxFrequencyPerWeek?: string;
|
maxFrequencyPerWeek?: string;
|
||||||
needleLengthMm?: string;
|
needleLengthMm?: string;
|
||||||
|
|
@ -114,17 +110,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="usage_notes">{m["productForm_usageNotes"]()}</Label>
|
|
||||||
<textarea
|
|
||||||
id="usage_notes"
|
|
||||||
name="usage_notes"
|
|
||||||
rows="2"
|
|
||||||
placeholder={m["productForm_usageNotesPlaceholder"]()}
|
|
||||||
class={textareaClass}
|
|
||||||
bind:value={usageNotes}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,8 +161,6 @@ export interface Product {
|
||||||
actives?: ActiveIngredient[];
|
actives?: ActiveIngredient[];
|
||||||
recommended_for: SkinType[];
|
recommended_for: SkinType[];
|
||||||
targets: SkinConcern[];
|
targets: SkinConcern[];
|
||||||
contraindications: string[];
|
|
||||||
usage_notes?: string;
|
|
||||||
fragrance_free?: boolean;
|
fragrance_free?: boolean;
|
||||||
essential_oils_free?: boolean;
|
essential_oils_free?: boolean;
|
||||||
alcohol_denat_free?: boolean;
|
alcohol_denat_free?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,6 @@ function parseOptionalString(v: string | null): string | undefined {
|
||||||
return s || undefined;
|
return s || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTextList(v: string | null): string[] {
|
|
||||||
if (!v?.trim()) return [];
|
|
||||||
return v.split(/\n/).map((s) => s.trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEffectProfile(form: FormData): Record<string, number> {
|
function parseEffectProfile(form: FormData): Record<string, number> {
|
||||||
const keys = [
|
const keys = [
|
||||||
'hydration_immediate', 'hydration_long_term',
|
'hydration_immediate', 'hydration_long_term',
|
||||||
|
|
@ -98,7 +93,6 @@ export const actions: Actions = {
|
||||||
const leave_on = form.get('leave_on') === 'true';
|
const leave_on = form.get('leave_on') === 'true';
|
||||||
const recommended_for = form.getAll('recommended_for') as string[];
|
const recommended_for = form.getAll('recommended_for') as string[];
|
||||||
const targets = form.getAll('targets') as string[];
|
const targets = form.getAll('targets') as string[];
|
||||||
const contraindications = parseTextList(form.get('contraindications') as string | null);
|
|
||||||
|
|
||||||
const inci_raw = form.get('inci') as string;
|
const inci_raw = form.get('inci') as string;
|
||||||
const inci = inci_raw
|
const inci = inci_raw
|
||||||
|
|
@ -113,13 +107,12 @@ export const actions: Actions = {
|
||||||
leave_on,
|
leave_on,
|
||||||
recommended_for,
|
recommended_for,
|
||||||
targets,
|
targets,
|
||||||
contraindications,
|
|
||||||
inci,
|
inci,
|
||||||
product_effect_profile: parseEffectProfile(form)
|
product_effect_profile: parseEffectProfile(form)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Optional strings
|
// Optional strings
|
||||||
for (const field of ['line_name', 'url', 'sku', 'barcode', 'usage_notes', 'personal_tolerance_notes', 'price_currency']) {
|
for (const field of ['line_name', 'url', 'sku', 'barcode', 'personal_tolerance_notes', 'price_currency']) {
|
||||||
const v = parseOptionalString(form.get(field) as string | null);
|
const v = parseOptionalString(form.get(field) as string | null);
|
||||||
body[field] = v ?? null;
|
body[field] = v ?? null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,6 @@ function parseOptionalString(v: string | null): string | undefined {
|
||||||
return s || undefined;
|
return s || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTextList(v: string | null): string[] {
|
|
||||||
if (!v?.trim()) return [];
|
|
||||||
return v.split(/\n/).map((s) => s.trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEffectProfile(form: FormData): Record<string, number> {
|
function parseEffectProfile(form: FormData): Record<string, number> {
|
||||||
const keys = [
|
const keys = [
|
||||||
'hydration_immediate', 'hydration_long_term',
|
'hydration_immediate', 'hydration_long_term',
|
||||||
|
|
@ -86,7 +81,6 @@ export const actions: Actions = {
|
||||||
const leave_on = form.get('leave_on') === 'true';
|
const leave_on = form.get('leave_on') === 'true';
|
||||||
const recommended_for = form.getAll('recommended_for') as string[];
|
const recommended_for = form.getAll('recommended_for') as string[];
|
||||||
const targets = form.getAll('targets') as string[];
|
const targets = form.getAll('targets') as string[];
|
||||||
const contraindications = parseTextList(form.get('contraindications') as string | null);
|
|
||||||
|
|
||||||
const inci_raw = form.get('inci') as string;
|
const inci_raw = form.get('inci') as string;
|
||||||
const inci = inci_raw
|
const inci = inci_raw
|
||||||
|
|
@ -101,13 +95,12 @@ export const actions: Actions = {
|
||||||
leave_on,
|
leave_on,
|
||||||
recommended_for,
|
recommended_for,
|
||||||
targets,
|
targets,
|
||||||
contraindications,
|
|
||||||
inci,
|
inci,
|
||||||
product_effect_profile: parseEffectProfile(form)
|
product_effect_profile: parseEffectProfile(form)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Optional strings
|
// Optional strings
|
||||||
for (const field of ['line_name', 'url', 'sku', 'barcode', 'usage_notes', 'personal_tolerance_notes', 'price_currency']) {
|
for (const field of ['line_name', 'url', 'sku', 'barcode', 'personal_tolerance_notes', 'price_currency']) {
|
||||||
const v = parseOptionalString(form.get(field) as string | null);
|
const v = parseOptionalString(form.get(field) as string | null);
|
||||||
if (v !== undefined) payload[field] = v;
|
if (v !== undefined) payload[field] = v;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue