Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/modules/commerce/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FakerError } from '../../errors/faker-error';
import { ModuleBase } from '../../internal/module-base';
import { calculateUPCCheckDigit } from './upc-check-digit';

// Source for official prefixes: https://www.isbn-international.org/range_file_generation
const ISBN_LENGTH_RULES: Record<
Expand Down Expand Up @@ -84,6 +85,8 @@ const ISBN_LENGTH_RULES: Record<
* For a department in a shop or product category, use [`department()`](https://fakerjs.dev/api/commerce.html#department).
*
* You can also create a price using [`price()`](https://fakerjs.dev/api/commerce.html#price).
*
* To work with product identifiers, generate an ISBN via [`isbn()`](https://fakerjs.dev/api/commerce.html#isbn) or a 12‑digit UPC via [`upc()`](https://fakerjs.dev/api/commerce.html#upc).
*/
export class CommerceModule extends ModuleBase {
/**
Expand Down Expand Up @@ -349,4 +352,51 @@ export class CommerceModule extends ModuleBase {

return data.join(separator);
}

/**
* Returns a valid [UPC‑A](https://en.wikipedia.org/wiki/Universal_Product_Code) (12 digits).
Comment thread
Dhanush-K-Gowda marked this conversation as resolved.
*
* When a `prefix` is provided, it is padded with random digits so that the body
* has 11 digits. The 12th digit (check digit) is computed using the Modulo 10 algorithm.
*
* @param options An options object.
* @param options.prefix Optional numeric prefix for the UPC body (0–11 digits).
*
* @returns A 12‑digit UPC‑A string.
*
* @throws {FakerError} If `prefix` contains non-digit characters or more than 11 digits.
*
* @example
* faker.commerce.upc() // '036000291452'
* faker.commerce.upc({ prefix: '01234' }) // '012345678905'
*
* @since 10.2.0
*/
upc(
options: {
/**
* Optional numeric prefix for the UPC body (0–11 digits).
*/
prefix?: string;
} = {}
): string {
const { prefix = '' } = options;
if (prefix && /\D/.test(prefix)) {
throw new FakerError('Prefix must contain only numeric digits');
}

if (prefix.length > 11) {
throw new FakerError('Prefix must be at most 11 numeric digits');
}

const remaining = 11 - prefix.length;
const rand = this.faker.string.numeric({
length: remaining,
allowLeadingZeros: true,
});

const body = `${prefix}${rand}`; // 11 digits
const check = calculateUPCCheckDigit(body);
return `${body}${check}`; // 12-digit UPC-A
}
}
32 changes: 32 additions & 0 deletions src/modules/commerce/upc-check-digit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FakerError } from '../../errors/faker-error';

/**
* Calculates the check digit for a UPC‑A using the Modulo 10 algorithm.
*
* @param digits The first 11 digits (UPC body) as a numeric string.
*
* @returns The check digit (0–9).
*
* @throws {FakerError} If `digits` is not exactly 11 numeric characters.
*
* @see upc
*
* @since 10.2.0
*/
export function calculateUPCCheckDigit(digits: string): number {
if (!/^\d{11}$/.test(digits)) {
throw new FakerError(
'calculateUPCCheckDigit expects exactly 11 numeric digits'
);
}

let sum = 0;
let idx = 0;
for (const digit of digits) {
const n = Number.parseInt(digit, 10);
sum += n * (idx % 2 === 0 ? 3 : 1);
idx++;
}

return (10 - (sum % 10)) % 10;
}
30 changes: 30 additions & 0 deletions test/modules/__snapshots__/commerce.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ exports[`commerce > 42 > productMaterial 1`] = `"Cotton"`;

exports[`commerce > 42 > productName 1`] = `"Handcrafted Wooden Sausages"`;

exports[`commerce > 42 > upc > noArgs 1`] = `"397511086709"`;

exports[`commerce > 42 > upc > with 5 digit prefix 1`] = `"012343975112"`;

exports[`commerce > 42 > upc > with 11 digit prefix 1`] = `"012345678905"`;

exports[`commerce > 42 > upc > with empty prefix 1`] = `"397511086709"`;

exports[`commerce > 42 > upc > with single digit prefix 1`] = `"039751108673"`;

exports[`commerce > 1211 > department 1`] = `"Tools"`;

exports[`commerce > 1211 > isbn > noArgs 1`] = `"978-1-82966-736-0"`;
Expand Down Expand Up @@ -72,6 +82,16 @@ exports[`commerce > 1211 > productMaterial 1`] = `"Steel"`;

exports[`commerce > 1211 > productName 1`] = `"Tasty Steel Cheese"`;

exports[`commerce > 1211 > upc > noArgs 1`] = `"982966736875"`;

exports[`commerce > 1211 > upc > with 5 digit prefix 1`] = `"012349829662"`;

exports[`commerce > 1211 > upc > with 11 digit prefix 1`] = `"012345678905"`;

exports[`commerce > 1211 > upc > with empty prefix 1`] = `"982966736875"`;

exports[`commerce > 1211 > upc > with single digit prefix 1`] = `"098296673688"`;

exports[`commerce > 1337 > department 1`] = `"Computers"`;

exports[`commerce > 1337 > isbn > noArgs 1`] = `"978-0-12-435297-1"`;
Expand Down Expand Up @@ -107,3 +127,13 @@ exports[`commerce > 1337 > productDescription 1`] = `"Innovative Car featuring l
exports[`commerce > 1337 > productMaterial 1`] = `"Ceramic"`;

exports[`commerce > 1337 > productName 1`] = `"Frozen Bronze Chicken"`;

exports[`commerce > 1337 > upc > noArgs 1`] = `"212435297133"`;

exports[`commerce > 1337 > upc > with 5 digit prefix 1`] = `"012342124351"`;

exports[`commerce > 1337 > upc > with 11 digit prefix 1`] = `"012345678905"`;

exports[`commerce > 1337 > upc > with empty prefix 1`] = `"212435297133"`;

exports[`commerce > 1337 > upc > with single digit prefix 1`] = `"021243529714"`;
222 changes: 222 additions & 0 deletions test/modules/commerce.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ import { times } from './../support/times';

const NON_SEEDED_BASED_RUN = 5;

/**
* Helper function to verify UPC check digit using Modulo 10 algorithm
*
* @param upc The UPC string to verify.
*/
function verifyUPCCheckDigit(upc: string): boolean {
if (!/^\d{12}$/.test(upc)) {
return false;
}

const body = upc.slice(0, 11);
const checkDigit = Number.parseInt(upc[11], 10);

let sum = 0;
let idx = 0;
for (const digit of body) {
const n = Number.parseInt(digit, 10);
sum += n * (idx % 2 === 0 ? 3 : 1);
idx++;
}

const calculatedCheck = (10 - (sum % 10)) % 10;
return calculatedCheck === checkDigit;
}
Comment thread
Dhanush-K-Gowda marked this conversation as resolved.

describe('commerce', () => {
seededTests(faker, 'commerce', (t) => {
t.itEach(
Expand Down Expand Up @@ -46,6 +71,14 @@ describe('commerce', () => {
})
.it('with space separators', { separator: ' ' });
});

t.describe('upc', (t) => {
t.it('noArgs')
.it('with empty prefix', { prefix: '' })
.it('with single digit prefix', { prefix: '0' })
.it('with 5 digit prefix', { prefix: '01234' })
.it('with 11 digit prefix', { prefix: '01234567890' });
});
});

describe.each(times(NON_SEEDED_BASED_RUN).map(() => faker.seed()))(
Expand Down Expand Up @@ -247,6 +280,195 @@ describe('commerce', () => {
expect(isbn).toSatisfy((isbn: string) => isISBN(isbn, 13));
});
});

describe(`upc()`, () => {
it('should return a 12-digit UPC-A string when not passing arguments', () => {
const upc = faker.commerce.upc();

expect(upc).toBeTruthy();
expect(upc).toBeTypeOf('string');
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
expect(upc, 'UPC should contain only digits').toMatch(/^\d{12}$/);
expect(
verifyUPCCheckDigit(upc),
'UPC check digit should be valid'
).toBe(true);
});

it('should return a 12-digit UPC-A string with empty prefix', () => {
const upc = faker.commerce.upc({ prefix: '' });

expect(upc).toBeTruthy();
expect(upc).toBeTypeOf('string');
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
expect(upc, 'UPC should contain only digits').toMatch(/^\d{12}$/);
expect(
verifyUPCCheckDigit(upc),
'UPC check digit should be valid'
).toBe(true);
});

it('should return a 12-digit UPC-A string with single digit prefix', () => {
const prefix = '0';
const upc = faker.commerce.upc({ prefix });

expect(upc).toBeTruthy();
expect(upc).toBeTypeOf('string');
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
expect(upc, 'UPC should contain only digits').toMatch(/^\d{12}$/);
expect(
upc.startsWith(prefix),
'UPC should start with the provided prefix'
).toBe(true);
expect(
verifyUPCCheckDigit(upc),
'UPC check digit should be valid'
).toBe(true);
});

it('should return a 12-digit UPC-A string with 5-digit prefix', () => {
const prefix = '01234';
const upc = faker.commerce.upc({ prefix });

expect(upc).toBeTruthy();
expect(upc).toBeTypeOf('string');
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
expect(upc, 'UPC should contain only digits').toMatch(/^\d{12}$/);
expect(
upc.startsWith(prefix),
'UPC should start with the provided prefix'
).toBe(true);
expect(
verifyUPCCheckDigit(upc),
'UPC check digit should be valid'
).toBe(true);
});

it('should return a 12-digit UPC-A string with 11-digit prefix', () => {
const prefix = '01234567890';
const upc = faker.commerce.upc({ prefix });

expect(upc).toBeTruthy();
expect(upc).toBeTypeOf('string');
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
expect(upc, 'UPC should contain only digits').toMatch(/^\d{12}$/);
expect(
upc.startsWith(prefix),
'UPC should start with the provided prefix'
).toBe(true);
expect(
verifyUPCCheckDigit(upc),
'UPC check digit should be valid'
).toBe(true);
Comment thread
Dhanush-K-Gowda marked this conversation as resolved.
Outdated
});

it('should handle prefix with leading zeros', () => {
const prefix = '00000';
const upc = faker.commerce.upc({ prefix });

expect(upc).toBeTruthy();
expect(upc, 'UPC should be exactly 12 digits').toHaveLength(12);
expect(upc.startsWith(prefix)).toBe(true);
expect(verifyUPCCheckDigit(upc)).toBe(true);
});

it('should generate different UPCs on multiple calls', () => {
const upc1 = faker.commerce.upc();
const upc2 = faker.commerce.upc();

// While it's theoretically possible to get the same UPC twice,
// it's highly unlikely with 11 digits of randomness
expect(upc1).not.toBe(upc2);
});
Comment thread
Dhanush-K-Gowda marked this conversation as resolved.
Outdated

it('should generate valid UPCs with various prefix lengths', () => {
const prefixLengths = [0, 1, 2, 5, 8, 11];

for (const length of prefixLengths) {
const prefix = length > 0 ? '0'.repeat(length) : '';
const upc = faker.commerce.upc({ prefix });

expect(
upc,
`UPC with prefix length ${length} should be 12 digits`
).toHaveLength(12);
expect(
verifyUPCCheckDigit(upc),
`UPC with prefix length ${length} should have valid check digit`
).toBe(true);
if (prefix) {
expect(
upc.startsWith(prefix),
`UPC should start with prefix of length ${length}`
).toBe(true);
}
}
});

it('should throw FakerError when prefix contains non-digit characters', () => {
expect(() => {
faker.commerce.upc({ prefix: 'abc' });
}).toThrow('Prefix must contain only numeric digits');

expect(() => {
faker.commerce.upc({ prefix: '123abc' });
}).toThrow('Prefix must contain only numeric digits');

expect(() => {
faker.commerce.upc({ prefix: '12-34' });
}).toThrow('Prefix must contain only numeric digits');

expect(() => {
faker.commerce.upc({ prefix: ' 123' });
}).toThrow('Prefix must contain only numeric digits');
});

it('should throw FakerError when prefix is longer than 11 digits', () => {
expect(() => {
faker.commerce.upc({ prefix: '012345678901' });
}).toThrow('Prefix must be at most 11 numeric digits');

expect(() => {
faker.commerce.upc({ prefix: '012345678901234' });
}).toThrow('Prefix must be at most 11 numeric digits');
});

it('should throw FakerError with correct error message for invalid prefix types', () => {
expect(() => {
faker.commerce.upc({ prefix: '12a' });
}).toThrow('Prefix must contain only numeric digits');

expect(() => {
faker.commerce.upc({ prefix: '012345678901' });
}).toThrow('Prefix must be at most 11 numeric digits');
});

it('should generate valid UPCs that pass check digit validation for multiple calls', () => {
const results = faker.helpers.multiple(() => faker.commerce.upc(), {
count: 100,
});

for (const upc of results) {
expect(upc).toHaveLength(12);
expect(upc).toMatch(/^\d{12}$/);
expect(verifyUPCCheckDigit(upc)).toBe(true);
}
});

it('should generate valid UPCs with prefix that pass check digit validation for multiple calls', () => {
const results = faker.helpers.multiple(
() => faker.commerce.upc({ prefix: '01234' }),
{ count: 100 }
);

for (const upc of results) {
expect(upc).toHaveLength(12);
expect(upc).toMatch(/^\d{12}$/);
expect(upc.startsWith('01234')).toBe(true);
expect(verifyUPCCheckDigit(upc)).toBe(true);
}
});
Comment thread
Dhanush-K-Gowda marked this conversation as resolved.
Outdated
});
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ exports[`check docs completeness > all modules and methods are present 1`] = `
"productDescription",
"productMaterial",
"productName",
"upc",
],
],
[
Expand Down