Merge pull request 'wine-characteristics' () from wine-characteristics into staging

Reviewed-on: 
This commit is contained in:
Otto 2025-04-09 12:46:00 +00:00
commit 1598ee8b0b
10 changed files with 169 additions and 38 deletions

@ -1,10 +1,11 @@
import { ObjectId } from 'mongodb'
import { RatingOptions } from '../types'
export class Review {
constructor(
public eventId: string, // foreign key referencing the nostrEvents collection
public productId: string, // unique identifier for the product
public rating: number, // numerical rating, e.g., 1-100
public rating: number | RatingOptions, // numerical rating, e.g., 84-100 or NS (no score)
public reviewText: string, // text content of the review
public tastingNotes: string[], // array of tasting notes, e.g., flavours, aromas
public id?: ObjectId // database object id

@ -6,7 +6,12 @@ import {
VintageOptions,
StandardDrinks,
WineRegion,
WineVolume
WineVolume,
WineStyle,
WhiteWineCharacteristic,
AmberWineCharacteristic,
RoseWineCharacteristic,
RedWineCharacteristic
} from '../types'
import { Alpha2Code } from 'i18n-iso-countries'
import { CurrencyCode } from 'currency-codes-ts/dist/types'
@ -17,8 +22,13 @@ export class Wine {
public productCodeUPC: string, // Product Code (https://en.wikipedia.org/wiki/Universal_Product_Code)
public productCodeSKU: string, // Stock keeping unit (https://en.wikipedia.org/wiki/Stock_keeping_unit)
public type: WineType, // numerical rating, e.g., 1-100
public style: string, // bubbles+fizz, table, dessert, fortified, vermouth
public characteristic: string, // light aromatic, textural, fruit forward, structural & savoury, powerful
public style: WineStyle, // bubbles+fizz, table, dessert, fortified, vermouth
public characteristics: (
| WhiteWineCharacteristic
| AmberWineCharacteristic
| RoseWineCharacteristic
| RedWineCharacteristic
)[], // light aromatic, textural, fruit forward, structural & savoury, powerful
public country: Alpha2Code, // two-letter country codes defined in ISO 3166-1 (https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
public region: WineRegion, // appellation, village, sub-region, vineyard
public name: string, // label

@ -1,10 +1,10 @@
export type CoffeeProcessingType =
| 'de-caff'
| 'honey'
| 'semi-dry'
| 'swiss water'
| 'sundried'
| 'washed'
| 'De-caff'
| 'Honey'
| 'Semi-dry'
| 'Swiss water'
| 'Sundried'
| 'Washed'
type CoffeeVarietyType = 'Robusta' | 'Arabica'

@ -1,4 +1,4 @@
export type Availability = 'in stock' | 'out of stock' | 'discontinued'
export type Availability = 'In stock' | 'Out of stock' | 'Discontinued'
export type Ingredient =
| 'Blanche'
@ -62,11 +62,15 @@ export type Ingredient =
| 'Walnut'
export enum VintageOptions {
NV = 'nv',
MV = 'mv'
NV = 'NV',
MV = 'MV'
}
export interface StandardDrinks {
'100ml': { AU: number; UK: number; US: number }
bottle: { AU: number; UK: number; US: number }
}
export enum RatingOptions {
NoScore = 'NS'
}

@ -1,9 +1,9 @@
export type SakeDesignation =
| 'table'
| 'pure'
| 'blended'
| 'mirin:new'
| 'mirin:true'
| 'mirin:salt'
| 'Table'
| 'Pure'
| 'Blended'
| 'Mirin:new'
| 'Mirin:true'
| 'Mirin:salt'
export type SakeStarter = 'Kimoto' | 'Sokujō' | 'Yamahai'

@ -1,4 +1,4 @@
export type SpiritType = 'white' | 'dark' | 'liqueurs'
export type SpiritType = 'White' | 'Dark' | 'Liqueurs'
export type SpiritVariant =
| 'Absinthe'
@ -77,7 +77,7 @@ export interface WhiteSpiritKind {
},
'Plymouth'
]
Mezcal: [{ Joven: ['espadín', 'tepeztate', 'Tequilana (blue)', 'tobalá'] }]
Mezcal: [{ Joven: ['Espadín', 'Tepeztate', 'Tequilana (blue)', 'Tobalá'] }]
Rum: ['Blanco', 'Cachaça', 'Platino', 'Agricole']
'Eau de Vie': [
'Apple (Pomme)',

@ -1,5 +1,5 @@
export enum UserRole {
User = 'user',
Reviewer = 'reviewer',
Producer = 'producer'
User = 'User',
Reviewer = 'Reviewer',
Producer = 'Producer'
}

@ -1,20 +1,53 @@
export enum WineType {
White = 'white',
Amber = 'amber',
Rose = 'rose',
Red = 'red'
White = 'White',
Amber = 'Amber',
Rose = 'Rose',
Red = 'Red'
}
export enum WhiteWineCharacteristic {
LightAromatic = 'Light Aromatic',
TexturalAndSavory = 'Textural and Savory',
RichAndFruitForward = 'Rich and Fruit Forward'
}
export enum AmberWineCharacteristic {
TexturalAndAromatic = 'Textural and Aromatic',
StructuralAndSavory = 'Structural and Savory',
PowerAndPresence = 'Power and Presence'
}
export enum RoseWineCharacteristic {
LightAndFruitForward = 'Light and Fruit Forward',
TexturalAndSavory = 'Textural and Savory',
StructuralAndAromatic = 'Structural and Aromatic'
}
export enum RedWineCharacteristic {
LightAndFruitForward = 'Light and Fruit Forward',
StructuralAndAromatic = 'Structural and Aromatic',
TexturalAndSavory = 'Textural and Savory',
PowerAndPresence = 'Power and Presence'
}
export enum WineStyle {
BubblesAndFizz = 'Bubbles + Fizz',
Table = 'Table',
Dessert = 'Dessert',
Fortified = 'Fortified',
Vermouth = 'Vermouth'
}
export enum Viticulture {
Biodynamic = 'biodynamic',
Organic = 'organic',
Conventional = 'conventional'
Biodynamic = 'Biodynamic',
Organic = 'Organic',
Conventional = 'Conventional'
}
export enum BottleClosure {
Cork = 'cork',
CrownSeal = 'crown-seal',
Screwcap = 'screwcap'
Cork = 'Cork',
CrownSeal = 'Crown-seal',
Screwcap = 'Screwcap'
}
export interface WineRegion {

@ -1,10 +1,16 @@
import Joi from 'joi'
import { RatingOptions } from '../../types'
export const reviewValidation = (data: unknown): Joi.ValidationResult =>
Joi.object({
eventId: Joi.string().required(),
productId: Joi.string().required(),
rating: Joi.number().required(),
rating: Joi.alternatives()
.try(
Joi.string().valid(...Object.values(RatingOptions)),
Joi.number().min(84).max(100)
)
.required(),
reviewText: Joi.string().required(),
tastingNotes: Joi.array().items(Joi.string())
}).validate(data)

@ -5,7 +5,12 @@ import {
BottleClosure,
Viticulture,
WineRegion,
WineVolume
WineVolume,
WineStyle,
WhiteWineCharacteristic,
AmberWineCharacteristic,
RoseWineCharacteristic,
RedWineCharacteristic
} from '../../types'
import { wineRegionsMap, isObject } from '../'
@ -17,8 +22,80 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
type: Joi.string()
.valid(...Object.values(WineType))
.required(),
style: Joi.string().required(),
characteristic: Joi.string().required(),
style: Joi.string()
.valid(...Object.values(WineStyle))
.required(),
characteristic: Joi.array()
.items(Joi.string())
.custom((value: string[], helper) => {
// return if no value
if (!value) {
return value
}
// return if no state ancestors
if (!helper.state.ancestors) {
return value
}
const wineType: WineType = helper.state.ancestors[0].type
// return if no wineType
if (!wineType) {
return value
}
let options: string[] = []
switch (wineType) {
case WineType.White:
{
options = Object.values(WhiteWineCharacteristic)
}
break
case WineType.Amber:
{
options = Object.values(AmberWineCharacteristic)
}
break
case WineType.Rose:
{
options = Object.values(RoseWineCharacteristic)
}
break
case WineType.Red:
{
options = Object.values(RedWineCharacteristic)
}
break
default:
break
}
if (!options.length) {
return helper.message({
custom: Joi.expression(
`no characteristics found for provided type of wine`
)
})
}
for (const characteristic of value) {
if (!options.includes(characteristic)) {
return helper.message({
custom: Joi.expression(
`"${characteristic}" is not a valid characteristic for "${wineType}" wine. Valid options are [${options.map((option) => `"${option}"`).join(', ')}]`
)
})
}
}
return value
}),
country: Joi.string().length(2),
region: Joi.alternatives()
.try(