mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
1161 lines
27 KiB
Markdown
1161 lines
27 KiB
Markdown
# @isa/utils/ean-validation
|
|
|
|
Lightweight Angular utility library for validating EAN (European Article Number) barcodes with reactive forms integration and standalone validation functions.
|
|
|
|
## Overview
|
|
|
|
The EAN Validation library provides tools for validating 13-digit EAN barcodes in Angular applications. It includes both an Angular Forms validator for reactive form validation and a standalone utility function for programmatic validation. The library uses a simple regex-based approach for fast, efficient validation without external dependencies.
|
|
|
|
## Table of Contents
|
|
|
|
- [Features](#features)
|
|
- [Quick Start](#quick-start)
|
|
- [Core Concepts](#core-concepts)
|
|
- [API Reference](#api-reference)
|
|
- [Usage Examples](#usage-examples)
|
|
- [EAN Format](#ean-format)
|
|
- [Architecture Notes](#architecture-notes)
|
|
- [Testing](#testing)
|
|
- [Dependencies](#dependencies)
|
|
|
|
## Features
|
|
|
|
- **Angular Forms validator** - `eanValidator` for reactive form controls
|
|
- **Standalone validation** - `isEan()` utility function for programmatic checks
|
|
- **Regex-based validation** - Fast, efficient 13-digit EAN validation
|
|
- **Zero dependencies** - No external libraries required
|
|
- **Type-safe** - Full TypeScript support
|
|
- **Null-safe** - Graceful handling of undefined/null values
|
|
- **Lightweight** - Minimal bundle size impact
|
|
- **Framework agnostic core** - Validation logic can be used outside Angular
|
|
|
|
## Quick Start
|
|
|
|
### 1. Reactive Form Validation
|
|
|
|
```typescript
|
|
import { Component } from '@angular/core';
|
|
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
import { eanValidator } from '@isa/utils/ean-validation';
|
|
|
|
@Component({
|
|
selector: 'app-product-form',
|
|
standalone: true,
|
|
imports: [ReactiveFormsModule],
|
|
template: `
|
|
<form [formGroup]="productForm">
|
|
<label>
|
|
EAN:
|
|
<input formControlName="ean" type="text" />
|
|
</label>
|
|
|
|
@if (eanControl.errors?.['invalidEan']) {
|
|
<div class="error">
|
|
Please enter a valid 13-digit EAN
|
|
</div>
|
|
}
|
|
</form>
|
|
`
|
|
})
|
|
export class ProductFormComponent {
|
|
productForm = new FormGroup({
|
|
ean: new FormControl('', [eanValidator])
|
|
});
|
|
|
|
get eanControl() {
|
|
return this.productForm.get('ean')!;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Programmatic Validation
|
|
|
|
```typescript
|
|
import { Component } from '@angular/core';
|
|
import { isEan } from '@isa/utils/ean-validation';
|
|
|
|
@Component({
|
|
selector: 'app-barcode-scanner',
|
|
template: `
|
|
<button (click)="validateBarcode('1234567890123')">
|
|
Validate Barcode
|
|
</button>
|
|
`
|
|
})
|
|
export class BarcodeScannerComponent {
|
|
validateBarcode(code: string): void {
|
|
if (isEan(code)) {
|
|
console.log('Valid EAN:', code);
|
|
// Process valid EAN
|
|
} else {
|
|
console.error('Invalid EAN:', code);
|
|
// Handle invalid EAN
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Combined Usage
|
|
|
|
```typescript
|
|
import { Component } from '@angular/core';
|
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
import { eanValidator, isEan } from '@isa/utils/ean-validation';
|
|
|
|
@Component({
|
|
selector: 'app-product-search',
|
|
standalone: true,
|
|
imports: [ReactiveFormsModule],
|
|
template: `
|
|
<input
|
|
[formControl]="searchControl"
|
|
placeholder="Enter EAN"
|
|
(keyup.enter)="search()"
|
|
/>
|
|
|
|
<button (click)="search()" [disabled]="!canSearch()">
|
|
Search
|
|
</button>
|
|
`
|
|
})
|
|
export class ProductSearchComponent {
|
|
searchControl = new FormControl('', [eanValidator]);
|
|
|
|
canSearch(): boolean {
|
|
const value = this.searchControl.value;
|
|
return !!value && isEan(value);
|
|
}
|
|
|
|
search(): void {
|
|
if (this.canSearch()) {
|
|
const ean = this.searchControl.value!;
|
|
console.log('Searching for:', ean);
|
|
// Perform search
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Core Concepts
|
|
|
|
### EAN (European Article Number)
|
|
|
|
EAN is a 13-digit barcode standard used internationally for product identification:
|
|
|
|
```
|
|
1234567890123
|
|
└───┬────┘││└─ Check digit
|
|
│ ││
|
|
│ │└── Product code
|
|
│ └─── Manufacturer code
|
|
└───────── Country/GS1 prefix
|
|
```
|
|
|
|
### Validation Rules
|
|
|
|
The library validates EANs based on these criteria:
|
|
|
|
1. **Length**: Must be exactly 13 digits
|
|
2. **Content**: Must contain only numeric characters (0-9)
|
|
3. **Format**: No spaces, dashes, or other separators allowed
|
|
|
|
**Valid EANs:**
|
|
```typescript
|
|
'1234567890123' ✓ (13 digits, all numeric)
|
|
'0000000000000' ✓ (13 digits, all numeric)
|
|
'9999999999999' ✓ (13 digits, all numeric)
|
|
```
|
|
|
|
**Invalid EANs:**
|
|
```typescript
|
|
'123456789012' ✗ (12 digits - too short)
|
|
'12345678901234' ✗ (14 digits - too long)
|
|
'123456789012A' ✗ (contains letter)
|
|
'1234-5678-9012' ✗ (contains dashes)
|
|
' 1234567890123' ✗ (contains whitespace)
|
|
'' ✗ (empty string - via isEan())
|
|
undefined ✗ (undefined - via isEan())
|
|
```
|
|
|
|
### Regex Pattern
|
|
|
|
The validation uses a simple regex pattern:
|
|
|
|
```typescript
|
|
const EAN_REGEX = /^[0-9]{13}$/;
|
|
```
|
|
|
|
**Pattern breakdown:**
|
|
- `^` - Start of string
|
|
- `[0-9]` - Any digit from 0 to 9
|
|
- `{13}` - Exactly 13 times
|
|
- `$` - End of string
|
|
|
|
## API Reference
|
|
|
|
### eanValidator
|
|
|
|
Angular Forms validator function for validating EAN input.
|
|
|
|
**Type:** `ValidatorFn`
|
|
|
|
**Signature:**
|
|
```typescript
|
|
eanValidator(control: AbstractControl): ValidationErrors | null
|
|
```
|
|
|
|
**Parameters:**
|
|
- `control: AbstractControl` - The form control to validate
|
|
|
|
**Returns:**
|
|
- `null` - If the value is valid or empty/null
|
|
- `{ invalidEan: true }` - If the value is invalid
|
|
|
|
**Validation Behavior:**
|
|
- **Empty values**: Returns `null` (considered valid - use `Validators.required` for required fields)
|
|
- **Valid EAN**: Returns `null`
|
|
- **Invalid EAN**: Returns `{ invalidEan: true }`
|
|
|
|
**Example:**
|
|
```typescript
|
|
import { FormControl } from '@angular/forms';
|
|
import { eanValidator } from '@isa/utils/ean-validation';
|
|
|
|
const eanControl = new FormControl('', [eanValidator]);
|
|
|
|
eanControl.setValue('1234567890123');
|
|
console.log(eanControl.valid); // true
|
|
|
|
eanControl.setValue('123');
|
|
console.log(eanControl.errors); // { invalidEan: true }
|
|
|
|
eanControl.setValue('');
|
|
console.log(eanControl.valid); // true (empty is valid)
|
|
```
|
|
|
|
### isEan
|
|
|
|
Standalone utility function for programmatic EAN validation.
|
|
|
|
**Signature:**
|
|
```typescript
|
|
isEan(value: string | undefined): boolean
|
|
```
|
|
|
|
**Parameters:**
|
|
- `value: string | undefined` - The value to validate
|
|
|
|
**Returns:**
|
|
- `true` - If the value is a valid 13-digit EAN
|
|
- `false` - If the value is invalid, undefined, null, or empty
|
|
|
|
**Validation Behavior:**
|
|
- **undefined/null**: Returns `false`
|
|
- **Empty string**: Returns `false`
|
|
- **Valid EAN**: Returns `true`
|
|
- **Invalid EAN**: Returns `false`
|
|
|
|
**Example:**
|
|
```typescript
|
|
import { isEan } from '@isa/utils/ean-validation';
|
|
|
|
isEan('1234567890123'); // true
|
|
isEan('123'); // false
|
|
isEan(''); // false
|
|
isEan(undefined); // false
|
|
isEan(null); // false (TypeScript allows via type coercion)
|
|
```
|
|
|
|
### EAN_REGEX
|
|
|
|
Regular expression constant for EAN validation.
|
|
|
|
**Type:** `RegExp`
|
|
|
|
**Value:** `/^[0-9]{13}$/`
|
|
|
|
**Note:** This is an internal constant. Prefer using `eanValidator` or `isEan()` for validation.
|
|
|
|
**Example:**
|
|
```typescript
|
|
import { EAN_REGEX } from '@isa/utils/ean-validation';
|
|
|
|
// Direct regex usage (not recommended)
|
|
const isValid = EAN_REGEX.test('1234567890123');
|
|
console.log(isValid); // true
|
|
|
|
// Prefer using isEan() instead
|
|
const isValidPreferred = isEan('1234567890123');
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Basic Form Validation
|
|
|
|
```typescript
|
|
import { Component } from '@angular/core';
|
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
import { eanValidator } from '@isa/utils/ean-validation';
|
|
|
|
@Component({
|
|
selector: 'app-product-form',
|
|
standalone: true,
|
|
imports: [ReactiveFormsModule],
|
|
template: `
|
|
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
|
<div class="form-field">
|
|
<label for="ean">Product EAN*</label>
|
|
<input
|
|
id="ean"
|
|
formControlName="ean"
|
|
type="text"
|
|
maxlength="13"
|
|
placeholder="1234567890123"
|
|
/>
|
|
|
|
@if (ean.touched && ean.errors) {
|
|
<div class="errors">
|
|
@if (ean.errors['required']) {
|
|
<p class="error">EAN is required</p>
|
|
}
|
|
@if (ean.errors['invalidEan']) {
|
|
<p class="error">EAN must be exactly 13 digits</p>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<button type="submit" [disabled]="!form.valid">
|
|
Save Product
|
|
</button>
|
|
</form>
|
|
`
|
|
})
|
|
export class ProductFormComponent {
|
|
form = new FormGroup({
|
|
ean: new FormControl('', [
|
|
Validators.required,
|
|
eanValidator
|
|
])
|
|
});
|
|
|
|
get ean() {
|
|
return this.form.get('ean')!;
|
|
}
|
|
|
|
onSubmit(): void {
|
|
if (this.form.valid) {
|
|
const productData = this.form.value;
|
|
console.log('Saving product:', productData);
|
|
// Save logic
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Barcode Scanner Integration
|
|
|
|
```typescript
|
|
import { Component, inject, signal } from '@angular/core';
|
|
import { isEan } from '@isa/utils/ean-validation';
|
|
|
|
interface ScanResult {
|
|
code: string;
|
|
valid: boolean;
|
|
timestamp: Date;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-barcode-scanner',
|
|
template: `
|
|
<div class="scanner">
|
|
<h2>Barcode Scanner</h2>
|
|
|
|
<button (click)="startScanning()">
|
|
Start Scanning
|
|
</button>
|
|
|
|
<div class="scan-history">
|
|
<h3>Scan History</h3>
|
|
@for (scan of scanHistory(); track scan.timestamp) {
|
|
<div
|
|
class="scan-result"
|
|
[class.valid]="scan.valid"
|
|
[class.invalid]="!scan.valid"
|
|
>
|
|
<span>{{ scan.code }}</span>
|
|
<span>{{ scan.valid ? '✓ Valid' : '✗ Invalid' }}</span>
|
|
<span>{{ scan.timestamp | date: 'short' }}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
`
|
|
})
|
|
export class BarcodeScannerComponent {
|
|
scanHistory = signal<ScanResult[]>([]);
|
|
|
|
startScanning(): void {
|
|
// Simulated barcode scan
|
|
this.processScan('1234567890123');
|
|
}
|
|
|
|
processScan(code: string): void {
|
|
const valid = isEan(code);
|
|
|
|
this.scanHistory.update(history => [
|
|
{
|
|
code,
|
|
valid,
|
|
timestamp: new Date()
|
|
},
|
|
...history
|
|
]);
|
|
|
|
if (valid) {
|
|
console.log('Valid EAN scanned:', code);
|
|
// Process valid EAN
|
|
this.lookupProduct(code);
|
|
} else {
|
|
console.error('Invalid EAN scanned:', code);
|
|
// Show error message
|
|
}
|
|
}
|
|
|
|
lookupProduct(ean: string): void {
|
|
// Product lookup logic
|
|
}
|
|
}
|
|
```
|
|
|
|
### Search with Validation
|
|
|
|
```typescript
|
|
import { Component, signal } from '@angular/core';
|
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
import { eanValidator, isEan } from '@isa/utils/ean-validation';
|
|
|
|
@Component({
|
|
selector: 'app-product-search',
|
|
standalone: true,
|
|
imports: [ReactiveFormsModule],
|
|
template: `
|
|
<div class="search-container">
|
|
<input
|
|
[formControl]="searchControl"
|
|
type="text"
|
|
placeholder="Enter 13-digit EAN"
|
|
maxlength="13"
|
|
(keyup.enter)="search()"
|
|
/>
|
|
|
|
<button
|
|
(click)="search()"
|
|
[disabled]="!isValidEan()"
|
|
>
|
|
Search
|
|
</button>
|
|
|
|
@if (searchControl.touched && searchControl.errors?.['invalidEan']) {
|
|
<p class="error">
|
|
Please enter a valid 13-digit EAN
|
|
</p>
|
|
}
|
|
|
|
@if (searchResult()) {
|
|
<div class="result">
|
|
<h3>Product Found</h3>
|
|
<p>EAN: {{ searchResult()!.ean }}</p>
|
|
<p>Name: {{ searchResult()!.name }}</p>
|
|
</div>
|
|
}
|
|
|
|
@if (notFound()) {
|
|
<p class="error">No product found with EAN: {{ lastSearch() }}</p>
|
|
}
|
|
</div>
|
|
`
|
|
})
|
|
export class ProductSearchComponent {
|
|
searchControl = new FormControl('', [eanValidator]);
|
|
|
|
searchResult = signal<{ ean: string; name: string } | null>(null);
|
|
notFound = signal(false);
|
|
lastSearch = signal('');
|
|
|
|
isValidEan(): boolean {
|
|
const value = this.searchControl.value;
|
|
return !!value && isEan(value);
|
|
}
|
|
|
|
search(): void {
|
|
if (!this.isValidEan()) return;
|
|
|
|
const ean = this.searchControl.value!;
|
|
this.lastSearch.set(ean);
|
|
|
|
// Simulated product lookup
|
|
const product = this.mockProductLookup(ean);
|
|
|
|
if (product) {
|
|
this.searchResult.set(product);
|
|
this.notFound.set(false);
|
|
} else {
|
|
this.searchResult.set(null);
|
|
this.notFound.set(true);
|
|
}
|
|
}
|
|
|
|
mockProductLookup(ean: string): { ean: string; name: string } | null {
|
|
// Mock implementation
|
|
return { ean, name: `Product ${ean}` };
|
|
}
|
|
}
|
|
```
|
|
|
|
### Batch Validation
|
|
|
|
```typescript
|
|
import { Component, signal } from '@angular/core';
|
|
import { isEan } from '@isa/utils/ean-validation';
|
|
|
|
interface ValidationResult {
|
|
ean: string;
|
|
valid: boolean;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-batch-validator',
|
|
template: `
|
|
<div class="batch-validator">
|
|
<h2>Batch EAN Validator</h2>
|
|
|
|
<textarea
|
|
[value]="inputEans()"
|
|
(input)="inputEans.set($any($event.target).value)"
|
|
placeholder="Enter EANs (one per line)"
|
|
rows="10"
|
|
></textarea>
|
|
|
|
<button (click)="validate()">Validate All</button>
|
|
|
|
@if (results().length > 0) {
|
|
<div class="results">
|
|
<h3>Validation Results</h3>
|
|
<p>
|
|
Valid: {{ validCount() }} / {{ results().length }}
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>EAN</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (result of results(); track result.ean) {
|
|
<tr [class.valid]="result.valid" [class.invalid]="!result.valid">
|
|
<td>{{ result.ean }}</td>
|
|
<td>{{ result.valid ? '✓ Valid' : '✗ Invalid' }}</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</div>
|
|
`
|
|
})
|
|
export class BatchValidatorComponent {
|
|
inputEans = signal('');
|
|
results = signal<ValidationResult[]>([]);
|
|
|
|
validCount = signal(0);
|
|
|
|
validate(): void {
|
|
const eans = this.inputEans()
|
|
.split('\n')
|
|
.map(line => line.trim())
|
|
.filter(line => line.length > 0);
|
|
|
|
const results: ValidationResult[] = eans.map(ean => ({
|
|
ean,
|
|
valid: isEan(ean)
|
|
}));
|
|
|
|
this.results.set(results);
|
|
this.validCount.set(results.filter(r => r.valid).length);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Dynamic Form with Multiple EANs
|
|
|
|
```typescript
|
|
import { Component } from '@angular/core';
|
|
import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
import { eanValidator } from '@isa/utils/ean-validation';
|
|
|
|
@Component({
|
|
selector: 'app-multi-ean-form',
|
|
standalone: true,
|
|
imports: [ReactiveFormsModule],
|
|
template: `
|
|
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
|
<h3>Product EANs</h3>
|
|
|
|
<div formArrayName="eans">
|
|
@for (control of eanControls.controls; track $index) {
|
|
<div class="ean-field">
|
|
<input
|
|
[formControlName]="$index"
|
|
type="text"
|
|
maxlength="13"
|
|
placeholder="1234567890123"
|
|
/>
|
|
|
|
@if (control.touched && control.errors?.['invalidEan']) {
|
|
<span class="error">Invalid EAN</span>
|
|
}
|
|
|
|
<button type="button" (click)="removeEan($index)">
|
|
Remove
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<button type="button" (click)="addEan()">
|
|
Add EAN
|
|
</button>
|
|
|
|
<button type="submit" [disabled]="!form.valid">
|
|
Save
|
|
</button>
|
|
</form>
|
|
`
|
|
})
|
|
export class MultiEanFormComponent {
|
|
form = new FormGroup({
|
|
eans: new FormArray([
|
|
new FormControl('', [eanValidator])
|
|
])
|
|
});
|
|
|
|
get eanControls() {
|
|
return this.form.get('eans') as FormArray;
|
|
}
|
|
|
|
addEan(): void {
|
|
this.eanControls.push(
|
|
new FormControl('', [eanValidator])
|
|
);
|
|
}
|
|
|
|
removeEan(index: number): void {
|
|
this.eanControls.removeAt(index);
|
|
}
|
|
|
|
onSubmit(): void {
|
|
if (this.form.valid) {
|
|
const eans = this.eanControls.value;
|
|
console.log('Saving EANs:', eans);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Type Guard Usage
|
|
|
|
```typescript
|
|
import { Component } from '@angular/core';
|
|
import { isEan } from '@isa/utils/ean-validation';
|
|
|
|
interface Product {
|
|
id: number;
|
|
name: string;
|
|
ean?: string; // Optional EAN
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-product-list',
|
|
template: `
|
|
<div class="products">
|
|
@for (product of products; track product.id) {
|
|
<div class="product">
|
|
<h3>{{ product.name }}</h3>
|
|
|
|
@if (hasValidEan(product)) {
|
|
<p>EAN: {{ product.ean }}</p>
|
|
} @else {
|
|
<p class="warning">No valid EAN</p>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
`
|
|
})
|
|
export class ProductListComponent {
|
|
products: Product[] = [
|
|
{ id: 1, name: 'Product A', ean: '1234567890123' },
|
|
{ id: 2, name: 'Product B', ean: '123' }, // Invalid
|
|
{ id: 3, name: 'Product C' }, // Missing
|
|
];
|
|
|
|
hasValidEan(product: Product): boolean {
|
|
return isEan(product.ean);
|
|
}
|
|
}
|
|
```
|
|
|
|
## EAN Format
|
|
|
|
### Standard Format
|
|
|
|
**EAN-13 Structure:**
|
|
```
|
|
┌──────────────────────────────────┐
|
|
│ 1 2 3 4 5 6 7 8 9 0 1 2 3 │
|
|
│ └──┬──┘ └───────┬────────┘ │ │ │
|
|
│ │ │ │ │ │
|
|
│ Prefix Manufacturer Product Check │
|
|
│ Code Code Digit │
|
|
└──────────────────────────────────┘
|
|
```
|
|
|
|
### Common Prefixes
|
|
|
|
| Prefix | Region/Type |
|
|
|--------|-------------|
|
|
| 000-019 | GS1 US |
|
|
| 020-029 | Restricted distribution |
|
|
| 030-039 | GS1 US |
|
|
| 040-049 | Restricted distribution |
|
|
| 050-059 | GS1 US reserved |
|
|
| 060-139 | GS1 US |
|
|
| 200-299 | Restricted distribution |
|
|
| 300-379 | GS1 France |
|
|
| 380 | GS1 Bulgaria |
|
|
| 383 | GS1 Slovenia |
|
|
| 385 | GS1 Croatia |
|
|
| 400-440 | GS1 Germany |
|
|
| 450-459, 490-499 | GS1 Japan |
|
|
| 460-469 | GS1 Russia |
|
|
| 470 | GS1 Kyrgyzstan |
|
|
| 471 | GS1 Taiwan |
|
|
| 474 | GS1 Estonia |
|
|
| 475 | GS1 Latvia |
|
|
| 476 | GS1 Azerbaijan |
|
|
| 477 | GS1 Lithuania |
|
|
| 478 | GS1 Uzbekistan |
|
|
| 479 | GS1 Sri Lanka |
|
|
| 480 | GS1 Philippines |
|
|
| 481 | GS1 Belarus |
|
|
| 482 | GS1 Ukraine |
|
|
| 484 | GS1 Moldova |
|
|
| 485 | GS1 Armenia |
|
|
| 486 | GS1 Georgia |
|
|
| 487 | GS1 Kazakhstan |
|
|
| 489 | GS1 Hong Kong |
|
|
| 500-509 | GS1 UK |
|
|
| 520-521 | GS1 Association Greece |
|
|
| 528 | GS1 Lebanon |
|
|
| 529 | GS1 Cyprus |
|
|
| 530 | GS1 Albania |
|
|
| 531 | GS1 Macedonia |
|
|
| 535 | GS1 Malta |
|
|
| 539 | GS1 Ireland |
|
|
| 540-549 | GS1 Belgium & Luxembourg |
|
|
| 560 | GS1 Portugal |
|
|
| 569 | GS1 Iceland |
|
|
| 570-579 | GS1 Denmark |
|
|
| 590 | GS1 Poland |
|
|
| 594 | GS1 Romania |
|
|
| 599 | GS1 Hungary |
|
|
| 600-601 | GS1 South Africa |
|
|
| 603 | GS1 Ghana |
|
|
| 604 | GS1 Senegal |
|
|
| 608 | GS1 Bahrain |
|
|
| 609 | GS1 Mauritius |
|
|
| 611 | GS1 Morocco |
|
|
| 613 | GS1 Algeria |
|
|
| 615 | GS1 Nigeria |
|
|
| 616 | GS1 Kenya |
|
|
| 618 | GS1 Ivory Coast |
|
|
| 619 | GS1 Tunisia |
|
|
| 620 | GS1 Tanzania |
|
|
| 621 | GS1 Syria |
|
|
| 622 | GS1 Egypt |
|
|
| 623 | GS1 Brunei |
|
|
| 624 | GS1 Libya |
|
|
| 625 | GS1 Jordan |
|
|
| 626 | GS1 Iran |
|
|
| 627 | GS1 Kuwait |
|
|
| 628 | GS1 Saudi Arabia |
|
|
| 629 | GS1 Emirates |
|
|
| 640-649 | GS1 Finland |
|
|
| 690-699 | GS1 China |
|
|
| 700-709 | GS1 Norway |
|
|
| 729 | GS1 Israel |
|
|
| 730-739 | GS1 Sweden |
|
|
| 740 | GS1 Guatemala |
|
|
| 741 | GS1 El Salvador |
|
|
| 742 | GS1 Honduras |
|
|
| 743 | GS1 Nicaragua |
|
|
| 744 | GS1 Costa Rica |
|
|
| 745 | GS1 Panama |
|
|
| 746 | GS1 Dominican Republic |
|
|
| 750 | GS1 Mexico |
|
|
| 754-755 | GS1 Canada |
|
|
| 759 | GS1 Venezuela |
|
|
| 760-769 | GS1 Switzerland |
|
|
| 770-771 | GS1 Colombia |
|
|
| 773 | GS1 Uruguay |
|
|
| 775 | GS1 Peru |
|
|
| 777 | GS1 Bolivia |
|
|
| 778-779 | GS1 Argentina |
|
|
| 780 | GS1 Chile |
|
|
| 784 | GS1 Paraguay |
|
|
| 786 | GS1 Ecuador |
|
|
| 789-790 | GS1 Brazil |
|
|
| 800-839 | GS1 Italy |
|
|
| 840-849 | GS1 Spain |
|
|
| 850 | GS1 Cuba |
|
|
| 858 | GS1 Slovakia |
|
|
| 859 | GS1 Czech |
|
|
| 860 | GS1 Serbia |
|
|
| 865 | GS1 Mongolia |
|
|
| 867 | GS1 North Korea |
|
|
| 868-869 | GS1 Turkey |
|
|
| 870-879 | GS1 Netherlands |
|
|
| 880 | GS1 South Korea |
|
|
| 884 | GS1 Cambodia |
|
|
| 885 | GS1 Thailand |
|
|
| 888 | GS1 Singapore |
|
|
| 890 | GS1 India |
|
|
| 893 | GS1 Vietnam |
|
|
| 896 | GS1 Pakistan |
|
|
| 899 | GS1 Indonesia |
|
|
| 900-919 | GS1 Austria |
|
|
| 930-939 | GS1 Australia |
|
|
| 940-949 | GS1 New Zealand |
|
|
| 950 | GS1 Global Office |
|
|
| 951 | GS1 Global Office (EPCglobal) |
|
|
| 955 | GS1 Malaysia |
|
|
| 958 | GS1 Macau |
|
|
| 960-969 | GS1 Global Office (GTIN-8) |
|
|
| 977 | Serial publications (ISSN) |
|
|
| 978-979 | Bookland (ISBN) |
|
|
| 980 | Refund receipts |
|
|
| 981-984 | GS1 coupon identification |
|
|
| 990-999 | GS1 coupon identification |
|
|
|
|
**Note:** This library validates format only (13 digits), not check digit accuracy or prefix validity.
|
|
|
|
## Architecture Notes
|
|
|
|
### Design Patterns
|
|
|
|
#### Validator Function Pattern
|
|
|
|
The `eanValidator` follows Angular's `ValidatorFn` pattern:
|
|
|
|
```typescript
|
|
export const eanValidator: ValidatorFn = (
|
|
control: AbstractControl
|
|
): ValidationErrors | null => {
|
|
const value = control.value;
|
|
if (value && !EAN_REGEX.test(value)) {
|
|
return { invalidEan: true };
|
|
}
|
|
return null;
|
|
};
|
|
```
|
|
|
|
**Benefits:**
|
|
- Composable with other validators
|
|
- Reusable across multiple form controls
|
|
- Type-safe with TypeScript
|
|
- Follows Angular conventions
|
|
|
|
#### Pure Function Pattern
|
|
|
|
The `isEan` function is a pure function with no side effects:
|
|
|
|
```typescript
|
|
export const isEan = (value: string | undefined): boolean => {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
return EAN_REGEX.test(value);
|
|
};
|
|
```
|
|
|
|
**Benefits:**
|
|
- Predictable - same input always produces same output
|
|
- Testable - easy to unit test
|
|
- Framework agnostic - can be used outside Angular
|
|
- No dependencies - doesn't rely on external state
|
|
|
|
### Performance Considerations
|
|
|
|
1. **Regex Performance** - Simple regex pattern for O(n) validation
|
|
2. **No External Dependencies** - Minimal bundle size impact
|
|
3. **Null-Safe** - Early returns for undefined/null values
|
|
4. **Reusable Regex** - Single compiled regex constant
|
|
|
|
### Future Enhancements
|
|
|
|
Potential improvements identified:
|
|
|
|
1. **Check Digit Validation** - Validate the EAN-13 check digit algorithm
|
|
2. **EAN-8 Support** - Support for 8-digit EAN format
|
|
3. **UPC Support** - Support for 12-digit UPC format
|
|
4. **Prefix Validation** - Validate GS1 country/region prefixes
|
|
5. **Format Conversion** - Convert between EAN-13, EAN-8, and UPC
|
|
6. **Barcode Generation** - Generate barcode images from EAN
|
|
7. **Async Validation** - Validate against product database
|
|
|
|
## Testing
|
|
|
|
The library uses **Vitest** with **Angular Testing Utilities** for testing.
|
|
|
|
### Running Tests
|
|
|
|
```bash
|
|
# Run tests for this library
|
|
npx nx test ean-validation --skip-nx-cache
|
|
|
|
# Run tests with coverage
|
|
npx nx test ean-validation --code-coverage --skip-nx-cache
|
|
|
|
# Run tests in watch mode
|
|
npx nx test ean-validation --watch
|
|
```
|
|
|
|
### Test Structure
|
|
|
|
The library includes comprehensive unit tests covering:
|
|
|
|
- **eanValidator Tests**:
|
|
- Valid 13-digit EAN returns null
|
|
- Empty value returns null (valid)
|
|
- Invalid EAN returns error object
|
|
- Null/undefined values handled correctly
|
|
|
|
- **isEan Tests**:
|
|
- Valid 13-digit EAN returns true
|
|
- Invalid EAN returns false
|
|
- Empty string returns false
|
|
- Undefined returns false
|
|
|
|
### Example Test
|
|
|
|
```typescript
|
|
import { describe, it, expect } from 'vitest';
|
|
import { FormControl } from '@angular/forms';
|
|
import { eanValidator, isEan } from './ean-validation';
|
|
|
|
describe('eanValidator', () => {
|
|
it('should return null for valid EAN', () => {
|
|
// Arrange
|
|
const control = new FormControl('1234567890123');
|
|
|
|
// Act
|
|
const result = eanValidator(control);
|
|
|
|
// Assert
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return null for empty value', () => {
|
|
// Arrange
|
|
const control = new FormControl('');
|
|
|
|
// Act
|
|
const result = eanValidator(control);
|
|
|
|
// Assert
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return error for invalid EAN', () => {
|
|
// Arrange
|
|
const control = new FormControl('123');
|
|
|
|
// Act
|
|
const result = eanValidator(control);
|
|
|
|
// Assert
|
|
expect(result).toEqual({ invalidEan: true });
|
|
});
|
|
});
|
|
|
|
describe('isEan', () => {
|
|
it('should return true for valid EAN', () => {
|
|
// Arrange
|
|
const validEan = '1234567890123';
|
|
|
|
// Act
|
|
const result = isEan(validEan);
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false for undefined value', () => {
|
|
// Arrange
|
|
const undefinedValue = undefined;
|
|
|
|
// Act
|
|
const result = isEan(undefinedValue);
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false for invalid EAN', () => {
|
|
// Arrange
|
|
const invalidEan = '123';
|
|
|
|
// Act
|
|
const result = isEan(invalidEan);
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Dependencies
|
|
|
|
### Required Libraries
|
|
|
|
- `@angular/core` - For TypeScript types
|
|
- `@angular/forms` - For `ValidatorFn`, `AbstractControl`, `ValidationErrors` types
|
|
|
|
### Path Alias
|
|
|
|
Import from: `@isa/utils/ean-validation`
|
|
|
|
### Exports
|
|
|
|
- `eanValidator` - Angular Forms validator function
|
|
- `isEan` - Standalone validation utility function
|
|
- `EAN_REGEX` - Regular expression constant (internal use)
|
|
|
|
## Best Practices
|
|
|
|
### 1. Use with Validators.required
|
|
|
|
Combine with `Validators.required` for required fields:
|
|
|
|
```typescript
|
|
// Good - Required + EAN validation
|
|
const eanControl = new FormControl('', [
|
|
Validators.required,
|
|
eanValidator
|
|
]);
|
|
|
|
// Avoid - Only EAN validation (allows empty)
|
|
const eanControl = new FormControl('', [eanValidator]);
|
|
```
|
|
|
|
### 2. Provide Clear Error Messages
|
|
|
|
Show user-friendly error messages:
|
|
|
|
```typescript
|
|
// Good - Clear, actionable error message
|
|
@if (ean.errors?.['invalidEan']) {
|
|
<p class="error">
|
|
Please enter a valid 13-digit EAN (e.g., 1234567890123)
|
|
</p>
|
|
}
|
|
|
|
// Avoid - Generic or unclear message
|
|
@if (ean.errors?.['invalidEan']) {
|
|
<p class="error">Invalid</p>
|
|
}
|
|
```
|
|
|
|
### 3. Use isEan for Type Guards
|
|
|
|
Use `isEan()` for runtime type checking:
|
|
|
|
```typescript
|
|
// Good - Type-safe validation
|
|
function processEan(code: string | undefined): void {
|
|
if (isEan(code)) {
|
|
// TypeScript knows code is string here
|
|
console.log('Processing:', code.toUpperCase());
|
|
}
|
|
}
|
|
|
|
// Avoid - Unsafe type assumption
|
|
function processEan(code: string): void {
|
|
// Might throw if code is invalid
|
|
console.log('Processing:', code.toUpperCase());
|
|
}
|
|
```
|
|
|
|
### 4. Add Maxlength Attribute
|
|
|
|
Prevent users from entering more than 13 digits:
|
|
|
|
```typescript
|
|
// Good - Limit input length
|
|
<input
|
|
formControlName="ean"
|
|
type="text"
|
|
maxlength="13"
|
|
placeholder="1234567890123"
|
|
/>
|
|
|
|
// Avoid - No length restriction
|
|
<input
|
|
formControlName="ean"
|
|
type="text"
|
|
/>
|
|
```
|
|
|
|
### 5. Handle Validation in Components
|
|
|
|
Keep validation logic in components, not templates:
|
|
|
|
```typescript
|
|
// Good - Logic in component
|
|
canSubmit(): boolean {
|
|
return this.form.valid && isEan(this.form.value.ean);
|
|
}
|
|
|
|
// Avoid - Complex logic in template
|
|
@if (form.valid && form.value.ean && /^[0-9]{13}$/.test(form.value.ean)) {
|
|
...
|
|
}
|
|
```
|
|
|
|
## Limitations
|
|
|
|
### What This Library Does NOT Do
|
|
|
|
1. **Check Digit Validation** - Does not validate the EAN-13 checksum digit
|
|
2. **Database Validation** - Does not verify if EAN exists in product database
|
|
3. **Prefix Validation** - Does not validate GS1 country/region prefixes
|
|
4. **Format Conversion** - Does not convert between EAN/UPC formats
|
|
5. **Barcode Generation** - Does not generate barcode images
|
|
|
|
### When to Use Additional Validation
|
|
|
|
For production systems, consider:
|
|
|
|
- **Check digit validation** for data integrity
|
|
- **Database lookup** for product existence
|
|
- **Prefix validation** for regional compliance
|
|
- **Duplicate detection** in batch processing
|
|
|
|
## License
|
|
|
|
Internal ISA Frontend library - not for external distribution.
|