Merge pull request 'feat: added spirit validation' (#40) from spirits-characteristics into staging
Reviewed-on: #40
This commit is contained in:
commit
2a743043e0
@ -1,5 +1,13 @@
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { SpiritType, SpiritVariant, Ingredient, VintageOptions } from '../types'
|
||||
import {
|
||||
SpiritType,
|
||||
SpiritVariant,
|
||||
Ingredient,
|
||||
VintageOptions,
|
||||
StandardDrinks,
|
||||
SpiritVolume,
|
||||
SpiritCharacteristics
|
||||
} from '../types'
|
||||
import { Alpha2Code } from 'i18n-iso-countries'
|
||||
import { CurrencyCode } from 'currency-codes-ts/dist/types'
|
||||
|
||||
@ -13,10 +21,12 @@ export class Spirit {
|
||||
public name: string, // label
|
||||
public producerId: ObjectId, // product producer
|
||||
public type: SpiritType, // spirit type
|
||||
public characteristics: SpiritCharacteristics, // light aromatic, textural, fruit forward, structural & savoury, powerful
|
||||
public variant: SpiritVariant, // vodka, rum, liqueur cream, etc
|
||||
public ingredients: Ingredient[], // an array of ingredients(flavouring)
|
||||
public volume: SpiritVolume, // bottle volume
|
||||
public alcohol: number, // alcohol percentage
|
||||
public standardDrinks100ml: number, // number representing an amount of standard drinks per bottle
|
||||
public standardDrinks: StandardDrinks, // an amount of standard drinks per 100ml and bottle in AU, UK and US
|
||||
public vintage: number | VintageOptions, // year, nv (non-vintage) or mv (multi-vintage)
|
||||
public RRPamount: number, // 20
|
||||
public RRPcurrency: CurrencyCode, // USD
|
||||
|
@ -1,6 +1,16 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { collections } from '../services/database.service'
|
||||
import { Spirit } from '../models'
|
||||
import {
|
||||
spiritValidation,
|
||||
productCodeValidation,
|
||||
handleReqError,
|
||||
handleReqSuccess,
|
||||
alcoholToStandardDrinks,
|
||||
volumeToMl
|
||||
} from '../utils'
|
||||
import Joi from 'joi'
|
||||
import { DBcollections, ProductType } from '../types'
|
||||
|
||||
export const spiritsRouter = express.Router()
|
||||
|
||||
@ -24,22 +34,35 @@ spiritsRouter.get('/', async (_req: Request, res: Response) => {
|
||||
// POST
|
||||
spiritsRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const spirit = req.body as Spirit
|
||||
const {
|
||||
error,
|
||||
value: spirit
|
||||
}: { error: Joi.ValidationError | undefined; value: Spirit } =
|
||||
spiritValidation(req.body)
|
||||
|
||||
const result = await collections.spirits?.insertOne(spirit)
|
||||
|
||||
if (result) {
|
||||
res
|
||||
.status(201)
|
||||
.send(`Successfully created a new spirit with id ${result.insertedId}`)
|
||||
} else {
|
||||
res.status(500).send('Failed to create a new spirit.')
|
||||
if (error) {
|
||||
throw error.details[0].message
|
||||
}
|
||||
|
||||
const { productCodeEAN, productCodeUPC, productCodeSKU } = spirit
|
||||
|
||||
await productCodeValidation(
|
||||
productCodeEAN,
|
||||
productCodeUPC,
|
||||
productCodeSKU,
|
||||
DBcollections.Spirits,
|
||||
ProductType.Spirit
|
||||
)
|
||||
|
||||
spirit.standardDrinks = alcoholToStandardDrinks(
|
||||
spirit.alcohol,
|
||||
volumeToMl(spirit.volume)
|
||||
)
|
||||
|
||||
const result = await collections[DBcollections.Spirits]?.insertOne(spirit)
|
||||
|
||||
handleReqSuccess(res, result, ProductType.Spirit)
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
res.status(400).send(error.message)
|
||||
}
|
||||
handleReqError(res, error)
|
||||
}
|
||||
})
|
||||
|
@ -3,12 +3,14 @@ import { collections } from '../services/database.service'
|
||||
import { Wine } from '../models'
|
||||
import {
|
||||
wineValidation,
|
||||
productCodeValidation,
|
||||
handleReqError,
|
||||
handleReqSuccess,
|
||||
alcoholToStandardDrinks,
|
||||
volumeToMl
|
||||
} from '../utils'
|
||||
import Joi from 'joi'
|
||||
import { DBcollections, ProductType } from '../types'
|
||||
|
||||
export const winesRouter = express.Router()
|
||||
|
||||
@ -17,7 +19,7 @@ winesRouter.use(express.json())
|
||||
// GET
|
||||
winesRouter.get('/', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const wines = await collections.wines?.find({}).toArray()
|
||||
const wines = await collections[DBcollections.Wines]?.find({}).toArray()
|
||||
|
||||
res.status(200).send(wines)
|
||||
} catch (error: unknown) {
|
||||
@ -45,46 +47,22 @@ winesRouter.post('/', async (req: Request, res: Response) => {
|
||||
|
||||
const { productCodeEAN, productCodeUPC, productCodeSKU } = wine
|
||||
|
||||
if (!productCodeEAN && !productCodeUPC && !productCodeSKU) {
|
||||
throw new Error(
|
||||
'provide "productCodeEAN", "productCodeUPC" or "productCodeSKU"'
|
||||
)
|
||||
}
|
||||
|
||||
if (productCodeEAN) {
|
||||
const existingWine = await collections.wines?.findOne({
|
||||
productCodeEAN
|
||||
})
|
||||
|
||||
if (existingWine) {
|
||||
throw new Error('wine with provided "productCodeEAN" exists')
|
||||
}
|
||||
} else if (productCodeUPC) {
|
||||
const existingWine = await collections.wines?.findOne({
|
||||
productCodeUPC
|
||||
})
|
||||
|
||||
if (existingWine) {
|
||||
throw new Error('wine with provided "productCodeUPC" exists')
|
||||
}
|
||||
} else {
|
||||
const existingWine = await collections.wines?.findOne({
|
||||
productCodeSKU
|
||||
})
|
||||
|
||||
if (existingWine) {
|
||||
throw new Error('wine with provided "productCodeSKU" exists')
|
||||
}
|
||||
}
|
||||
await productCodeValidation(
|
||||
productCodeEAN,
|
||||
productCodeUPC,
|
||||
productCodeSKU,
|
||||
DBcollections.Wines,
|
||||
ProductType.Wine
|
||||
)
|
||||
|
||||
wine.standardDrinks = alcoholToStandardDrinks(
|
||||
wine.alcohol,
|
||||
volumeToMl(wine.volume)
|
||||
)
|
||||
|
||||
const result = await collections.wines?.insertOne(wine)
|
||||
const result = await collections[DBcollections.Wines]?.insertOne(wine)
|
||||
|
||||
handleReqSuccess(res, result, 'wine')
|
||||
handleReqSuccess(res, result, ProductType.Wine)
|
||||
} catch (error: unknown) {
|
||||
handleReqError(res, error)
|
||||
}
|
||||
|
@ -1,65 +1,65 @@
|
||||
export type Availability = 'In stock' | 'Out of stock' | 'Discontinued'
|
||||
|
||||
export type Ingredient =
|
||||
| 'Blanche'
|
||||
| 'Anise'
|
||||
| 'Fennel'
|
||||
| 'Hyssop'
|
||||
| 'Mint'
|
||||
| 'Citrus Peel'
|
||||
| 'Coriander Seeds'
|
||||
| 'Angelica Root'
|
||||
| 'Cinnamon'
|
||||
| 'Clove'
|
||||
| 'Wheat'
|
||||
| 'Rye'
|
||||
| 'Corn'
|
||||
| 'Potato'
|
||||
| 'Barley'
|
||||
| 'Sugarcane'
|
||||
| 'Fruits'
|
||||
| 'Grains'
|
||||
| 'Juniper'
|
||||
| 'Coriander'
|
||||
| 'Lemon peel'
|
||||
| 'Orange peel'
|
||||
| 'Orris root'
|
||||
| 'Cassia bark'
|
||||
| 'Licorice root'
|
||||
| 'Grapefruit peel'
|
||||
| 'Elderflower'
|
||||
| 'Apple'
|
||||
| 'Blackcurrant'
|
||||
| 'Butterscotch'
|
||||
| 'Peach'
|
||||
| 'Pear'
|
||||
| 'Plum'
|
||||
| 'Raspberries'
|
||||
| 'Sorghum'
|
||||
| 'Rice'
|
||||
| 'Millet'
|
||||
| 'Brown sugar'
|
||||
| 'Buckwheat'
|
||||
| 'Sweet Potato'
|
||||
| 'Oat'
|
||||
| 'Egg (Advocaat)'
|
||||
| 'Strawberry'
|
||||
| 'Almond'
|
||||
| 'Banana'
|
||||
| 'Chocolate'
|
||||
| 'Sour Cherry'
|
||||
| 'Violet'
|
||||
| 'Lemon'
|
||||
| 'Melon'
|
||||
| 'Orange'
|
||||
| 'Raspberry'
|
||||
| 'Yuzu'
|
||||
| 'Almond'
|
||||
| 'Apricot Kernel'
|
||||
| 'Hazelnut'
|
||||
| 'Peanut'
|
||||
| 'Pecan'
|
||||
| 'Walnut'
|
||||
export enum Ingredient {
|
||||
Blanche = 'Blanche',
|
||||
Anise = 'Anise',
|
||||
Fennel = 'Fennel',
|
||||
Hyssop = 'Hyssop',
|
||||
Mint = 'Mint',
|
||||
CitrusPeel = 'Citrus Peel',
|
||||
CorianderSeeds = 'Coriander Seeds',
|
||||
AngelicaRoot = 'Angelica Root',
|
||||
Cinnamon = 'Cinnamon',
|
||||
Clove = 'Clove',
|
||||
Wheat = 'Wheat',
|
||||
Rye = 'Rye',
|
||||
Corn = 'Corn',
|
||||
Potato = 'Potato',
|
||||
Barley = 'Barley',
|
||||
Sugarcane = 'Sugarcane',
|
||||
Fruits = 'Fruits',
|
||||
Grains = 'Grains',
|
||||
Juniper = 'Juniper',
|
||||
Coriander = 'Coriander',
|
||||
LemonPeel = 'Lemon peel',
|
||||
OrangePeel = 'Orange peel',
|
||||
OrrisRoot = 'Orris root',
|
||||
CassiaBark = 'Cassia bark',
|
||||
LicoriceRoot = 'Licorice root',
|
||||
GrapefruitPeel = 'Grapefruit peel',
|
||||
Elderflower = 'Elderflower',
|
||||
Apple = 'Apple',
|
||||
Blackcurrant = 'Blackcurrant',
|
||||
Butterscotch = 'Butterscotch',
|
||||
Peach = 'Peach',
|
||||
Pear = 'Pear',
|
||||
Plum = 'Plum',
|
||||
Raspberries = 'Raspberries',
|
||||
Sorghum = 'Sorghum',
|
||||
Rice = 'Rice',
|
||||
Millet = 'Millet',
|
||||
BrownSugar = 'Brown sugar',
|
||||
Buckwheat = 'Buckwheat',
|
||||
SweetPotato = 'Sweet Potato',
|
||||
Oat = 'Oat',
|
||||
EggAdvocaat = 'Egg (Advocaat)',
|
||||
Strawberry = 'Strawberry',
|
||||
Almond = 'Almond',
|
||||
Banana = 'Banana',
|
||||
Chocolate = 'Chocolate',
|
||||
SourCherry = 'Sour Cherry',
|
||||
Violet = 'Violet',
|
||||
Lemon = 'Lemon',
|
||||
Melon = 'Melon',
|
||||
Orange = 'Orange',
|
||||
Raspberry = 'Raspberry',
|
||||
Yuzu = 'Yuzu',
|
||||
ApricotKernel = 'Apricot Kernel',
|
||||
Hazelnut = 'Hazelnut',
|
||||
Peanut = 'Peanut',
|
||||
Pecan = 'Pecan',
|
||||
Walnut = 'Walnut'
|
||||
}
|
||||
|
||||
export enum VintageOptions {
|
||||
NV = 'NV',
|
||||
@ -74,3 +74,10 @@ export interface StandardDrinks {
|
||||
export enum RatingOptions {
|
||||
NoScore = 'NS'
|
||||
}
|
||||
|
||||
export enum ProductType {
|
||||
Wine = 'Wine',
|
||||
Spirit = 'Spirit',
|
||||
Sake = 'Sake',
|
||||
Coffee = 'Coffee'
|
||||
}
|
||||
|
@ -1,172 +1,90 @@
|
||||
export type SpiritType = 'White' | 'Dark' | 'Liqueurs'
|
||||
|
||||
export type SpiritVariant =
|
||||
| 'Absinthe'
|
||||
| 'Pastis'
|
||||
| 'Vodka'
|
||||
| 'Genever'
|
||||
| 'Gin'
|
||||
| 'Mezcal'
|
||||
| 'Rum'
|
||||
| 'Eau de Vie'
|
||||
| 'Grappa'
|
||||
| 'Baijiu'
|
||||
| 'Soju'
|
||||
| 'Absinthe'
|
||||
| 'Brandy'
|
||||
| 'Calvados'
|
||||
| 'Chartreuse'
|
||||
| 'Genever'
|
||||
| 'Mezcal'
|
||||
| 'Rum'
|
||||
| 'Slivovitz'
|
||||
| 'Whiskey'
|
||||
| 'Amaro'
|
||||
| 'Coffee'
|
||||
| 'Cream'
|
||||
| 'Creme'
|
||||
| 'Flowers'
|
||||
| 'Fruit'
|
||||
| 'Herb'
|
||||
| 'Honey'
|
||||
| 'Nut'
|
||||
|
||||
export interface WhiteSpiritKind {
|
||||
Absinthe: ['Blanche']
|
||||
Pastis: [
|
||||
'Anise',
|
||||
'Fennel',
|
||||
'Licorice Root',
|
||||
'Hyssop',
|
||||
'Mint',
|
||||
'Citrus Peel',
|
||||
'Coriander Seeds',
|
||||
'Angelica Root',
|
||||
'Cinnamon',
|
||||
'Clove'
|
||||
]
|
||||
Vodka: [
|
||||
'Wheat',
|
||||
'Rye',
|
||||
'Corn',
|
||||
'Potato',
|
||||
'Barley',
|
||||
'Sugarcane',
|
||||
'Fruits',
|
||||
'Grains'
|
||||
]
|
||||
Genever: [
|
||||
{
|
||||
Young: 'Juniper'
|
||||
}
|
||||
]
|
||||
Gin: [
|
||||
{
|
||||
'London Dry': [
|
||||
'Juniper',
|
||||
'Coriander',
|
||||
'Angelica root',
|
||||
'Lemon peel',
|
||||
'Orange peel',
|
||||
'Orris root',
|
||||
'Cassia bark',
|
||||
'Licorice root',
|
||||
'Grapefruit peel',
|
||||
'Elderflower'
|
||||
]
|
||||
},
|
||||
'Plymouth'
|
||||
]
|
||||
Mezcal: [{ Joven: ['Espadín', 'Tepeztate', 'Tequilana (blue)', 'Tobalá'] }]
|
||||
Rum: ['Blanco', 'Cachaça', 'Platino', 'Agricole']
|
||||
'Eau de Vie': [
|
||||
'Apple (Pomme)',
|
||||
'Blackcurrant (Kirsch)',
|
||||
'Butterscotch (Schnapps)',
|
||||
'Peach (Pêche, Schnapps)',
|
||||
'Pear (Poire William)',
|
||||
'Plum (Mirabelle, Slivovitz, Rakia)',
|
||||
'Raspberries (Framboise)'
|
||||
]
|
||||
Grappa: ['Marc', 'Pisco']
|
||||
Baijiu: ['Sorghum', 'Wheat', 'Barley', 'Rice', 'Millet']
|
||||
Soju: ['Barley', 'Brown sugar', 'Buckwheat', 'Rice', 'Sweet Potato']
|
||||
export enum SpiritType {
|
||||
White = 'White',
|
||||
Dark = 'Dark',
|
||||
Liqueurs = 'Liqueurs'
|
||||
}
|
||||
|
||||
export interface DarkSpiritKind {
|
||||
Absinthe: ['Jaune', 'Verte']
|
||||
Brandy: [
|
||||
{
|
||||
Grape: [
|
||||
'VS',
|
||||
'VSOP',
|
||||
'XO',
|
||||
'Beyond Age',
|
||||
'Solera',
|
||||
'Solera Reserva',
|
||||
'Solera Gran Reserva'
|
||||
]
|
||||
}
|
||||
]
|
||||
Calvados: ['Apple', 'Pear']
|
||||
Chartreuse: ['Green', 'Yellow']
|
||||
Genever: [{ Old: ['Juniper'] }, { Coren: ['Juniper'] }]
|
||||
Mezcal: ['Reposado', 'Abuelo', 'Añejo', 'Extra Añejo']
|
||||
Rum: [
|
||||
{
|
||||
Sugar: [
|
||||
'Cachaca (amarela/ouro)',
|
||||
'Dark Rum',
|
||||
'Gold Rum',
|
||||
'Over-proof',
|
||||
'Premium',
|
||||
'Spiced'
|
||||
]
|
||||
}
|
||||
]
|
||||
Slivovitz: []
|
||||
Whiskey: ['Barley', 'Rye', 'Wheat', 'Corn', 'Oat', 'Rice']
|
||||
export enum SpiritVariant {
|
||||
Absinthe = 'Absinthe',
|
||||
Pastis = 'Pastis',
|
||||
Vodka = 'Vodka',
|
||||
Gin = 'Gin',
|
||||
Mezcal = 'Mezcal',
|
||||
Eau = 'Eau de Vie',
|
||||
Grappa = 'Grappa',
|
||||
Baijiu = 'Baijiu',
|
||||
Soju = 'Soju',
|
||||
Brandy = 'Brandy',
|
||||
Calvados = 'Calvados',
|
||||
Chartreuse = 'Chartreuse',
|
||||
Genever = 'Genever',
|
||||
Rum = 'Rum',
|
||||
Slivovitz = 'Slivovitz',
|
||||
Whiskey = 'Whiskey',
|
||||
Amaro = 'Amaro',
|
||||
Coffee = 'Coffee',
|
||||
Cream = 'Cream',
|
||||
Creme = 'Creme',
|
||||
Flowers = 'Flowers',
|
||||
Fruit = 'Fruit',
|
||||
Herb = 'Herb',
|
||||
Honey = 'Honey',
|
||||
Nut = 'Nut'
|
||||
}
|
||||
|
||||
export interface LiqueursSpiritKind {
|
||||
Amaro: []
|
||||
Coffee: []
|
||||
Cream: [
|
||||
'Egg (Advocaat)',
|
||||
'Amarula',
|
||||
'Rum',
|
||||
'Strawberry',
|
||||
'Whiskey (Baileys etc)'
|
||||
]
|
||||
Creme: [
|
||||
'Almond',
|
||||
'Banana',
|
||||
'Blackcurrant',
|
||||
'Chocolate',
|
||||
'Peach',
|
||||
'Sour Cherry',
|
||||
'Violet'
|
||||
]
|
||||
Flowers: ['Rose', 'Violet', 'Elderflower']
|
||||
Fruit: [
|
||||
'Blackcurrant',
|
||||
'Lemon',
|
||||
'Melon',
|
||||
'Orange',
|
||||
'Peach',
|
||||
'Plum',
|
||||
'Raspberry',
|
||||
'Yuzu'
|
||||
]
|
||||
Herb: [
|
||||
'Anise',
|
||||
'Dom Benedictine',
|
||||
'Bitters',
|
||||
'Ginger',
|
||||
'Jägermeister',
|
||||
'Metaxa',
|
||||
'Mint'
|
||||
]
|
||||
Honey: ['Licor 43', 'Rum', 'Vodka', 'Whiskey']
|
||||
Nut: ['Almond', 'Apricot Kernel', 'Hazelnut', 'Peanut', 'Pecan', 'Walnut']
|
||||
export enum SpiritVolume {
|
||||
'0.05L' = '0.05L',
|
||||
'0.15L' = '0.15L',
|
||||
'0.25L' = '0.25L',
|
||||
'0.375L' = '0.375L',
|
||||
'0.5L' = '0.5L',
|
||||
'0.7L' = '0.7L',
|
||||
'1L' = '1L'
|
||||
}
|
||||
|
||||
export enum SpiritCharacteristics {
|
||||
LightAndNeutral = 'Light and Neutral',
|
||||
FruityAndAromatic = 'Fruity and Aromatic',
|
||||
HerbalAndBotanical = 'Herbal and Botanical',
|
||||
SweetAndSyrupy = 'Sweet and Syrupy',
|
||||
SmokyAndSpicy = 'Smoky and Spicy',
|
||||
RichAndFullBodied = 'Rich and Full-Bodied'
|
||||
}
|
||||
|
||||
export enum WhiteSpiritVariants {
|
||||
Absinthe = 'Absinthe',
|
||||
Pastis = 'Pastis',
|
||||
Vodka = 'Vodka',
|
||||
Genever = 'Genever',
|
||||
Gin = 'Gin',
|
||||
Mezcal = 'Mezcal',
|
||||
Rum = 'Rum',
|
||||
EauDeVie = 'Eau de Vie',
|
||||
Grappa = 'Grappa',
|
||||
Baijiu = 'Baijiu',
|
||||
Soju = 'Soju'
|
||||
}
|
||||
|
||||
export enum DarkSpiritVariants {
|
||||
Absinthe = 'Absinthe',
|
||||
Brandy = 'Brandy',
|
||||
Calvados = 'Calvados',
|
||||
Chartreuse = 'Chartreuse',
|
||||
Genever = 'Genever',
|
||||
Mezcal = 'Mezcal',
|
||||
Rum = 'Rum',
|
||||
Slivovitz = 'Slivovitz',
|
||||
Whiskey = 'Whiskey'
|
||||
}
|
||||
|
||||
export enum LiqueursSpiritVariants {
|
||||
Amaro = 'Amaro',
|
||||
Coffee = 'Coffee',
|
||||
Cream = 'Cream',
|
||||
Creme = 'Creme',
|
||||
Flowers = 'Flowers',
|
||||
Fruit = 'Fruit',
|
||||
Herb = 'Herb',
|
||||
Honey = 'Honey',
|
||||
Nut = 'Nut'
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
export enum UserRole {
|
||||
User = 'User',
|
||||
Reviewer = 'Reviewer',
|
||||
Producer = 'Producer'
|
||||
Producer = 'Producer' // not able to leave a review
|
||||
}
|
||||
|
@ -67,3 +67,5 @@ export enum WineVolume {
|
||||
'6L' = '6L',
|
||||
'12L' = '12L'
|
||||
}
|
||||
|
||||
// retsina greek wine with pine essences should be considered a vermouth
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { StandardDrinks, WineVolume } from '../types'
|
||||
import { SpiritVolume, StandardDrinks, WineVolume } from '../types'
|
||||
import { roundToOneDecimal } from './'
|
||||
|
||||
export const alcoholToStandardDrinks = (
|
||||
@ -25,7 +25,7 @@ export const alcoholToStandardDrinks = (
|
||||
}
|
||||
}
|
||||
|
||||
export const volumeToMl = (volume: WineVolume): number => {
|
||||
export const volumeToMl = (volume: WineVolume | SpiritVolume): number => {
|
||||
if (volume.endsWith('L')) {
|
||||
const volumeMl = volume.replace('L', '')
|
||||
|
||||
|
@ -4,3 +4,4 @@ export * from './route'
|
||||
export * from './utils'
|
||||
export * from './alcohol'
|
||||
export * from './wine'
|
||||
export * from './spirit'
|
||||
|
250
src/utils/spirit.ts
Normal file
250
src/utils/spirit.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import {
|
||||
SpiritCharacteristics,
|
||||
SpiritType,
|
||||
WhiteSpiritVariants,
|
||||
DarkSpiritVariants,
|
||||
LiqueursSpiritVariants
|
||||
} from '../types'
|
||||
|
||||
// TODO: improve types
|
||||
export const spiritVariantMap: {
|
||||
[key in SpiritType]:
|
||||
| {
|
||||
[key in WhiteSpiritVariants]: (string | { [key: string]: string[] })[]
|
||||
}
|
||||
| {
|
||||
[key in DarkSpiritVariants]: (string | { [key: string]: string[] })[]
|
||||
}
|
||||
| {
|
||||
[key in LiqueursSpiritVariants]: (
|
||||
| string
|
||||
| { [key: string]: string[] }
|
||||
)[]
|
||||
}
|
||||
} = {
|
||||
[SpiritType.White]: {
|
||||
[WhiteSpiritVariants.Absinthe]: ['Blanche'],
|
||||
[WhiteSpiritVariants.Pastis]: [
|
||||
'Anise',
|
||||
'Fennel',
|
||||
'Licorice Root',
|
||||
'Hyssop',
|
||||
'Mint',
|
||||
'Citrus Peel',
|
||||
'Coriander Seeds',
|
||||
'Angelica Root',
|
||||
'Cinnamon',
|
||||
'Clove'
|
||||
],
|
||||
[WhiteSpiritVariants.Vodka]: [
|
||||
'Wheat',
|
||||
'Rye',
|
||||
'Corn',
|
||||
'Potato',
|
||||
'Barley',
|
||||
'Sugarcane',
|
||||
'Fruits',
|
||||
'Grains'
|
||||
],
|
||||
[WhiteSpiritVariants.Genever]: [
|
||||
{
|
||||
Young: ['Juniper']
|
||||
}
|
||||
],
|
||||
[WhiteSpiritVariants.Gin]: [
|
||||
{
|
||||
'London Dry': [
|
||||
'Juniper',
|
||||
'Coriander',
|
||||
'Angelica root',
|
||||
'Lemon peel',
|
||||
'Orange peel',
|
||||
'Orris root',
|
||||
'Cassia bark',
|
||||
'Licorice root',
|
||||
'Grapefruit peel',
|
||||
'Elderflower'
|
||||
]
|
||||
},
|
||||
'Plymouth'
|
||||
],
|
||||
[WhiteSpiritVariants.Mezcal]: [
|
||||
{ Joven: ['Espadín', 'Tepeztate', 'Tequilana (blue)', 'Tobalá'] }
|
||||
],
|
||||
[WhiteSpiritVariants.Rum]: ['Blanco', 'Cachaça', 'Platino', 'Agricole'],
|
||||
[WhiteSpiritVariants.EauDeVie]: [
|
||||
'Apple (Pomme)',
|
||||
'Blackcurrant (Kirsch)',
|
||||
'Butterscotch (Schnapps)',
|
||||
'Peach (Pêche, Schnapps)',
|
||||
'Pear (Poire William)',
|
||||
'Plum (Mirabelle, Slivovitz, Rakia)',
|
||||
'Raspberries (Framboise)'
|
||||
],
|
||||
[WhiteSpiritVariants.Grappa]: ['Marc', 'Pisco'],
|
||||
[WhiteSpiritVariants.Baijiu]: [
|
||||
'Sorghum',
|
||||
'Wheat',
|
||||
'Barley',
|
||||
'Rice',
|
||||
'Millet'
|
||||
],
|
||||
[WhiteSpiritVariants.Soju]: [
|
||||
'Barley',
|
||||
'Brown sugar',
|
||||
'Buckwheat',
|
||||
'Rice',
|
||||
'Sweet Potato'
|
||||
]
|
||||
},
|
||||
[SpiritType.Dark]: {
|
||||
[DarkSpiritVariants.Absinthe]: ['Jaune', 'Verte'],
|
||||
[DarkSpiritVariants.Brandy]: [
|
||||
{
|
||||
Grape: [
|
||||
'VS',
|
||||
'VSOP',
|
||||
'XO',
|
||||
'Beyond Age',
|
||||
'Solera',
|
||||
'Solera Reserva',
|
||||
'Solera Gran Reserva'
|
||||
]
|
||||
}
|
||||
],
|
||||
[DarkSpiritVariants.Calvados]: ['Apple', 'Pear'],
|
||||
[DarkSpiritVariants.Chartreuse]: ['Green', 'Yellow'],
|
||||
[DarkSpiritVariants.Genever]: [
|
||||
{ Old: ['Juniper'] },
|
||||
{ Coren: ['Juniper'] }
|
||||
],
|
||||
[DarkSpiritVariants.Mezcal]: ['Reposado', 'Abuelo', 'Añejo', 'Extra Añejo'],
|
||||
[DarkSpiritVariants.Rum]: [
|
||||
{
|
||||
Sugar: [
|
||||
'Cachaca (amarela/ouro)',
|
||||
'Dark Rum',
|
||||
'Gold Rum',
|
||||
'Over-proof',
|
||||
'Premium',
|
||||
'Spiced'
|
||||
]
|
||||
}
|
||||
],
|
||||
[DarkSpiritVariants.Slivovitz]: [],
|
||||
[DarkSpiritVariants.Whiskey]: [
|
||||
'Barley',
|
||||
'Rye',
|
||||
'Wheat',
|
||||
'Corn',
|
||||
'Oat',
|
||||
'Rice'
|
||||
]
|
||||
},
|
||||
[SpiritType.Liqueurs]: {
|
||||
[LiqueursSpiritVariants.Amaro]: [],
|
||||
[LiqueursSpiritVariants.Coffee]: [],
|
||||
[LiqueursSpiritVariants.Cream]: [
|
||||
'Egg (Advocaat)',
|
||||
'Amarula',
|
||||
'Rum',
|
||||
'Strawberry',
|
||||
'Whiskey (Baileys etc)'
|
||||
],
|
||||
[LiqueursSpiritVariants.Creme]: [
|
||||
'Almond',
|
||||
'Banana',
|
||||
'Blackcurrant',
|
||||
'Chocolate',
|
||||
'Peach',
|
||||
'Sour Cherry',
|
||||
'Violet'
|
||||
],
|
||||
[LiqueursSpiritVariants.Flowers]: ['Rose', 'Violet', 'Elderflower'],
|
||||
[LiqueursSpiritVariants.Fruit]: [
|
||||
'Blackcurrant',
|
||||
'Lemon',
|
||||
'Melon',
|
||||
'Orange',
|
||||
'Peach',
|
||||
'Plum',
|
||||
'Raspberry',
|
||||
'Yuzu'
|
||||
],
|
||||
[LiqueursSpiritVariants.Herb]: [
|
||||
'Anise',
|
||||
'Dom Benedictine',
|
||||
'Bitters',
|
||||
'Ginger',
|
||||
'Jägermeister',
|
||||
'Metaxa',
|
||||
'Mint'
|
||||
],
|
||||
[LiqueursSpiritVariants.Honey]: ['Licor 43', 'Rum', 'Vodka', 'Whiskey'],
|
||||
[LiqueursSpiritVariants.Nut]: [
|
||||
'Almond',
|
||||
'Apricot Kernel',
|
||||
'Hazelnut',
|
||||
'Peanut',
|
||||
'Pecan',
|
||||
'Walnut'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const spiritCharacteristicsMap: {
|
||||
[key in SpiritCharacteristics]: string[]
|
||||
} = {
|
||||
[SpiritCharacteristics.LightAndNeutral]: [
|
||||
WhiteSpiritVariants.Mezcal,
|
||||
WhiteSpiritVariants.Soju,
|
||||
WhiteSpiritVariants.Vodka,
|
||||
WhiteSpiritVariants.Rum
|
||||
],
|
||||
[SpiritCharacteristics.FruityAndAromatic]: [
|
||||
WhiteSpiritVariants.Rum,
|
||||
DarkSpiritVariants.Calvados,
|
||||
WhiteSpiritVariants.EauDeVie,
|
||||
LiqueursSpiritVariants.Fruit,
|
||||
WhiteSpiritVariants.Gin,
|
||||
WhiteSpiritVariants.Grappa
|
||||
],
|
||||
[SpiritCharacteristics.HerbalAndBotanical]: [
|
||||
WhiteSpiritVariants.Absinthe,
|
||||
DarkSpiritVariants.Absinthe,
|
||||
LiqueursSpiritVariants.Amaro,
|
||||
WhiteSpiritVariants.Genever,
|
||||
DarkSpiritVariants.Genever,
|
||||
WhiteSpiritVariants.Gin,
|
||||
WhiteSpiritVariants.Pastis,
|
||||
DarkSpiritVariants.Chartreuse
|
||||
],
|
||||
|
||||
[SpiritCharacteristics.SweetAndSyrupy]: [
|
||||
DarkSpiritVariants.Brandy,
|
||||
LiqueursSpiritVariants.Cream,
|
||||
LiqueursSpiritVariants.Creme,
|
||||
DarkSpiritVariants.Rum,
|
||||
LiqueursSpiritVariants.Nut
|
||||
],
|
||||
[SpiritCharacteristics.SmokyAndSpicy]: [
|
||||
WhiteSpiritVariants.Baijiu,
|
||||
DarkSpiritVariants.Rum,
|
||||
WhiteSpiritVariants.Gin,
|
||||
WhiteSpiritVariants.EauDeVie,
|
||||
DarkSpiritVariants.Mezcal,
|
||||
DarkSpiritVariants.Whiskey,
|
||||
LiqueursSpiritVariants.Cream,
|
||||
LiqueursSpiritVariants.Honey
|
||||
],
|
||||
[SpiritCharacteristics.RichAndFullBodied]: [
|
||||
WhiteSpiritVariants.Baijiu,
|
||||
DarkSpiritVariants.Brandy,
|
||||
DarkSpiritVariants.Rum,
|
||||
WhiteSpiritVariants.Grappa,
|
||||
WhiteSpiritVariants.EauDeVie,
|
||||
LiqueursSpiritVariants.Cream,
|
||||
LiqueursSpiritVariants.Honey,
|
||||
DarkSpiritVariants.Whiskey
|
||||
]
|
||||
}
|
@ -2,3 +2,5 @@ export * from './user'
|
||||
export * from './nostr'
|
||||
export * from './review'
|
||||
export * from './wine'
|
||||
export * from './spirit'
|
||||
export * from './product'
|
||||
|
46
src/utils/validation/product.ts
Normal file
46
src/utils/validation/product.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { collections } from '../../services/database.service'
|
||||
import { DBcollections, ProductType } from '../../types'
|
||||
|
||||
export const productCodeValidation = async (
|
||||
ean: string,
|
||||
upc: string,
|
||||
sku: string,
|
||||
collection: DBcollections,
|
||||
productType: ProductType
|
||||
) => {
|
||||
if (!ean && !upc && !sku) {
|
||||
throw new Error(
|
||||
'provide "productCodeEAN", "productCodeUPC" or "productCodeSKU"'
|
||||
)
|
||||
}
|
||||
|
||||
if (ean) {
|
||||
const existingProduct = await collections[collection]?.findOne({
|
||||
productCodeEAN: ean
|
||||
})
|
||||
|
||||
if (existingProduct) {
|
||||
throw new Error(`${productType} with provided "productCodeEAN" exists`)
|
||||
}
|
||||
}
|
||||
|
||||
if (upc) {
|
||||
const existingProduct = await collections[collection]?.findOne({
|
||||
productCodeUPC: upc
|
||||
})
|
||||
|
||||
if (existingProduct) {
|
||||
throw new Error(`${productType} with provided "productCodeUPC" exists`)
|
||||
}
|
||||
}
|
||||
|
||||
if (sku) {
|
||||
const existingProduct = await collections[collection]?.findOne({
|
||||
productCodeSKU: sku
|
||||
})
|
||||
|
||||
if (existingProduct) {
|
||||
throw new Error(`${productType} with provided "productCodeSKU" exists`)
|
||||
}
|
||||
}
|
||||
}
|
288
src/utils/validation/spirit.ts
Normal file
288
src/utils/validation/spirit.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
Ingredient,
|
||||
SpiritType,
|
||||
SpiritVolume,
|
||||
VintageOptions,
|
||||
SpiritCharacteristics
|
||||
} from '../../types'
|
||||
import { isObject, spiritVariantMap, spiritCharacteristicsMap } from '../'
|
||||
|
||||
export const spiritValidation = (data: unknown): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
productCodeEAN: Joi.string().allow('').required(),
|
||||
productCodeUPC: Joi.string().allow('').required(),
|
||||
productCodeSKU: Joi.string().allow('').required(),
|
||||
country: Joi.string().length(2),
|
||||
region: Joi.string(),
|
||||
name: Joi.string().required(),
|
||||
producerId: Joi.string().length(24).required(),
|
||||
type: Joi.string()
|
||||
.valid(...Object.values(SpiritType))
|
||||
.required(),
|
||||
characteristics: Joi.array()
|
||||
.items(
|
||||
Joi.string().custom((characteristic, helper) => {
|
||||
if (!Object.values(SpiritCharacteristics).includes(characteristic)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${characteristic}" is not a valid characteristic. Valid options are [${Object.values(
|
||||
SpiritCharacteristics
|
||||
)
|
||||
.map((option) => `"${option}"`)
|
||||
.join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const spiritType: SpiritType = helper.state.ancestors[1].type
|
||||
|
||||
const spiritVariant: string | { [key: string]: unknown } =
|
||||
helper.state.ancestors[1].variant
|
||||
|
||||
const spiritVariantName =
|
||||
typeof spiritVariant === 'string'
|
||||
? spiritVariant
|
||||
: Object.keys(spiritVariant)[0]
|
||||
|
||||
const variantsInCharacteristic =
|
||||
spiritCharacteristicsMap[characteristic as SpiritCharacteristics]
|
||||
|
||||
const characteristicsInVariant = Object.keys(
|
||||
spiritCharacteristicsMap
|
||||
).filter((char) =>
|
||||
spiritCharacteristicsMap[char as SpiritCharacteristics].includes(
|
||||
spiritVariantName
|
||||
)
|
||||
)
|
||||
|
||||
if (!variantsInCharacteristic.includes(spiritVariantName)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${characteristic}" is not a valid characteristic for "${spiritType} -> ${spiritVariantName}". Valid options are [${characteristicsInVariant
|
||||
.map((option) => `"${option}"`)
|
||||
.join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return characteristic
|
||||
})
|
||||
)
|
||||
.required(),
|
||||
variant: Joi.alternatives().try(
|
||||
Joi.string().custom((variant, helper) => {
|
||||
// return if no value
|
||||
if (!variant) {
|
||||
return variant
|
||||
}
|
||||
// return if no state ancestors
|
||||
if (!helper.state.ancestors) {
|
||||
return variant
|
||||
}
|
||||
|
||||
const spiritType: SpiritType = helper.state.ancestors[0].type
|
||||
|
||||
// return if no spiritType
|
||||
if (!spiritType) {
|
||||
return variant
|
||||
}
|
||||
|
||||
const options: string[] = Object.keys(spiritVariantMap[spiritType])
|
||||
|
||||
if (!options.length) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`no variants found for provided type of spirit`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (!options.includes(variant)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${variant}" is not a valid variant for "${spiritType}" spirit. Valid options are [${options.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return variant
|
||||
}),
|
||||
Joi.object().custom(
|
||||
(
|
||||
variant: { [key: string]: string | { [key: string]: string } },
|
||||
helper
|
||||
) => {
|
||||
// return if no value
|
||||
if (!variant) {
|
||||
return variant
|
||||
}
|
||||
|
||||
// return if multiple variant provided
|
||||
if (Object.keys(variant).length !== 1) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(`multiple variants provided`)
|
||||
})
|
||||
}
|
||||
|
||||
// return if no state ancestors
|
||||
if (!helper.state.ancestors) {
|
||||
return variant
|
||||
}
|
||||
|
||||
const spiritType: SpiritType = helper.state.ancestors[0].type
|
||||
|
||||
// return if no spiritType
|
||||
if (!spiritType) {
|
||||
return variant
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant
|
||||
*/
|
||||
const variantOptions: string[] = Object.keys(
|
||||
spiritVariantMap[spiritType]
|
||||
)
|
||||
|
||||
if (!variantOptions.length) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`no variants found for provided type of spirit`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const variantName = Object.keys(variant)[0]
|
||||
|
||||
if (!variantOptions.includes(variantName)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${variantName}" is not a valid variant for "${spiritType}" spirit. Valid options are [${variantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* SubVariant
|
||||
*/
|
||||
const subVariants: (string | { [key: string]: string[] })[] = (
|
||||
spiritVariantMap[spiritType] as {
|
||||
[key: string]: (string | { [key: string]: string[] })[]
|
||||
}
|
||||
)[variantName]
|
||||
|
||||
const subVariantOptions: string[] =
|
||||
typeof subVariants[0] === 'string'
|
||||
? (subVariants as string[])
|
||||
: subVariants.map((subVariant) => Object.keys(subVariant)[0])
|
||||
|
||||
const subVariantValues = Object.values(variant)
|
||||
|
||||
if (subVariantValues.length !== 1) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${variantName}" is not a valid variant for "${spiritType}" spirit. Valid options are [${variantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const subVariant = subVariantValues[0]
|
||||
|
||||
if (
|
||||
typeof subVariant === 'string' &&
|
||||
!subVariantOptions.includes(subVariant)
|
||||
) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${subVariant}" is not a valid variant for "${spiritType} -> ${variantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
} else if (isObject(subVariant)) {
|
||||
const providedSubVariants = Object.keys(subVariant)
|
||||
|
||||
if (providedSubVariants.length !== 1) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${subVariant}" is not a valid variant for "${spiritType} -> ${variantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const subVariantName = providedSubVariants[0]
|
||||
|
||||
if (!subVariantOptions.includes(subVariantName)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${subVariantName}" is not a valid variant for "${spiritType} -> ${variantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* SubSubVariant
|
||||
*/
|
||||
if (typeof subVariants[0] === 'string') {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`Provided not valid variant for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const subSubVariantOption = (
|
||||
subVariants as { [key: string]: string[] }[]
|
||||
).find((subVariant) => subVariant[subVariantName] !== undefined)
|
||||
|
||||
if (!subSubVariantOption) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${subVariantName}" is not a valid variant for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const subSubVariantOptions: string[] =
|
||||
subSubVariantOption[subVariantName]
|
||||
|
||||
const providedSubSubVariant = Object.values(subVariant)[0]
|
||||
|
||||
if (typeof providedSubSubVariant !== 'string') {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`Provided variant is not valid for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subSubVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (!subSubVariantOptions.includes(providedSubSubVariant)) {
|
||||
return helper.message({
|
||||
custom: Joi.expression(
|
||||
`"${providedSubSubVariant}" is not a valid variant for "${spiritType} -> ${variantName} -> ${subVariantName}" spirit. Valid options are [${subSubVariantOptions.map((option) => `"${option}"`).join(', ')}]`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return variant
|
||||
}
|
||||
)
|
||||
),
|
||||
ingredients: Joi.array()
|
||||
.items(Joi.string().valid(...Object.values(Ingredient)))
|
||||
.required(),
|
||||
volume: Joi.string()
|
||||
.valid(...Object.values(SpiritVolume))
|
||||
.required(),
|
||||
alcohol: Joi.number().min(0).max(0.99).required(),
|
||||
vintage: Joi.alternatives()
|
||||
.try(
|
||||
Joi.string().valid(...Object.values(VintageOptions)),
|
||||
Joi.number().min(1000).max(new Date().getFullYear())
|
||||
)
|
||||
.required(),
|
||||
RRPamount: Joi.number().required(),
|
||||
RRPcurrency: Joi.string().length(3).required(),
|
||||
description: Joi.string().required(),
|
||||
url: Joi.string(),
|
||||
image: Joi.string()
|
||||
}).validate(data)
|
@ -25,7 +25,7 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
|
||||
style: Joi.string()
|
||||
.valid(...Object.values(WineStyle))
|
||||
.required(),
|
||||
characteristic: Joi.array()
|
||||
characteristics: Joi.array()
|
||||
.items(Joi.string())
|
||||
.custom((value: string[], helper) => {
|
||||
// return if no value
|
||||
@ -97,6 +97,7 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
|
||||
return value
|
||||
}),
|
||||
country: Joi.string().length(2),
|
||||
// TODO: improve types
|
||||
region: Joi.alternatives()
|
||||
.try(
|
||||
Joi.string().custom((value, helper) => {
|
||||
@ -359,7 +360,10 @@ export const wineValidation = (data: unknown): Joi.ValidationResult =>
|
||||
producerId: Joi.string().length(24).required(),
|
||||
varietal: Joi.string().required(),
|
||||
vintage: Joi.alternatives()
|
||||
.try(Joi.string().valid(...Object.values(VintageOptions)), Joi.number())
|
||||
.try(
|
||||
Joi.string().valid(...Object.values(VintageOptions)),
|
||||
Joi.number().min(1000).max(new Date().getFullYear())
|
||||
)
|
||||
.required(),
|
||||
volume: Joi.string()
|
||||
.valid(...Object.values(WineVolume))
|
||||
|
@ -389,13 +389,13 @@ export const wineRegionsMap: { [key: string]: string[] | WineRegion } = {
|
||||
'Bitlis',
|
||||
'İzmir',
|
||||
'Manisa',
|
||||
'Ayvalık'
|
||||
'Ayvalik'
|
||||
],
|
||||
'Aegean Region': ['Urla', 'Foça', 'Bornova'],
|
||||
Ayvalık: [],
|
||||
Ayvalik: [],
|
||||
'Marmara Region': ['İstanbul', 'Edirne'],
|
||||
'Black Sea Region': ['Rize', 'Artvin', 'Trabzon'],
|
||||
'Southeastern Anatolia': ['Gaziantep', 'Şanlıurfa', 'Adana']
|
||||
'Southeastern Anatolia': ['Gaziantep', 'Şanliurfa', 'Adana']
|
||||
},
|
||||
AM: ['Vayots Dzor', 'Ararat Valley', 'Gegharkunik', 'Tavush', 'Syunik'],
|
||||
CA: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user