├── .babelrc ├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .sequelizerc ├── Dockerfile ├── README.md ├── database └── dump.sql ├── package-lock.json ├── package.json ├── src ├── controllers │ ├── __tests__ │ │ ├── attributes.controller.test.js │ │ ├── customer.controller.test.js │ │ ├── product.controller.test.js │ │ ├── shipping.controller.test.js │ │ ├── shoppingCart.controller.test.js │ │ └── tax.controller.test.js │ ├── attributes.controller.js │ ├── customer.controller.js │ ├── product.controller.js │ ├── shipping.controller.js │ ├── shoppingCart.controller.js │ └── tax.controller.js ├── database │ ├── config │ │ └── config.js │ └── models │ │ ├── attribute.js │ │ ├── attributeValue.js │ │ ├── category.js │ │ ├── customer.js │ │ ├── department.js │ │ ├── index.js │ │ ├── order.js │ │ ├── orderDetail.js │ │ ├── product.js │ │ ├── productAttribute.js │ │ ├── productCategory.js │ │ ├── shipping.js │ │ ├── shippingRegion.js │ │ ├── shoppingCart.js │ │ └── tax.js ├── index.js ├── public │ └── index.html ├── routes │ ├── api │ │ ├── attribute.route.js │ │ ├── customer.route.js │ │ ├── index.js │ │ ├── product.route.js │ │ ├── shipping.route.js │ │ ├── shoppingCart.route.js │ │ ├── tax.route.js │ │ └── welcome.route.js │ └── index.js └── test │ ├── helpers.js │ └── migrate.js └── turing-entrypoint.sh /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ## Create a .env file and copy these variables in it 2 | 3 | ## Setup your mysql development database 4 | 5 | DB_USER= turing 6 | DB_PASS= turing 7 | DB_NAME= turing 8 | DB_HOST= 127.0.0.1 9 | 10 | ## Setup your mysql test database here 11 | 12 | TEST_DB_USER= test database username here 13 | TEST_DB_PASS= test database password here 14 | TEST_DB_NAME= test database name here 15 | TEST_DB_HOST= test database host here 16 | 17 | ## Production database URI variable 18 | DATABASE_URL= production database URI here in url format 19 | 20 | JWT_KEY= JWT secret key here, any random key 21 | 22 | ## Register on stripe payment website to get stripe keys below 23 | STRIPE_PUBLISHABLE_KEY= get this from stripe website 24 | STRIPE_SECRET_KEY= get this from stripe website 25 | 26 | ## Register on sendgrid website to get sendgrid API key 27 | SENDGRID_API_KEY= get this from sendgrid 28 | 29 | ## Any random key for session secret 30 | SESSION_SECRET= any random secret key -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_module/ 2 | /dist/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"] 6 | }, 7 | "env": { 8 | "jest": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .DS_Store 5 | .env 6 | 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directory 33 | node_modules 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | #Local environment 42 | .env 43 | dist/ 44 | .vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | "config": path.resolve('./src/database/config', 'config.js'), 5 | "migrations-path": path.resolve('./src/database/migrations'), 6 | 'models-path': path.resolve('./src/database/models'), 7 | 'seeders-path': path.resolve('./src/database/seeders') 8 | }; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:5.7 2 | MAINTAINER Peter Adeoye 3 | 4 | # SETUP DATABASES 5 | ENV MYSQL_DATABASE 'turing' 6 | ENV MYSQL_ROOT_PASSWORD 'root' 7 | ENV MYSQL_USER 'turing' 8 | ENV MYSQL_PASSWORD 'turing' 9 | 10 | COPY ./database/dump.sql /docker-entrypoint-initdb.d/dump.sql 11 | 12 | ENV NODE_ENV=development 13 | COPY package.json package-lock.json ./ 14 | 15 | RUN apt-get update && \ 16 | apt-get install curl software-properties-common make -y && \ 17 | curl -sL https://deb.nodesource.com/setup_12.x | bash - 18 | 19 | RUN apt-get update && \ 20 | apt-get install -y \ 21 | nodejs 22 | 23 | RUN apt-get install build-essential -y 24 | 25 | RUN mkdir /backend 26 | WORKDIR / 27 | 28 | COPY package-*.json . 29 | RUN npm install 30 | 31 | COPY . . 32 | 33 | EXPOSE 80 34 | COPY turing-entrypoint.sh /turing-entrypoint.sh 35 | 36 | CMD ["sh", "turing-entrypoint.sh"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Turing Back End Challenge 2 | To complete this challenge, you need to ensure all route returns a similar response object as described in our API guide. 3 | To achieve this goal 4 | - You will have to fix the existing bugs 5 | - Implement the incomplete functions, 6 | - Add test cases for the main functions of the system. 7 | - Add Dockerfile to the root of the project to run the app in docker environment 8 | 9 | 10 | ## Getting started 11 | 12 | ### Prerequisites 13 | 14 | In order to install and run this project locally, you would need to have the following installed on you local machine. 15 | 16 | * [**Node JS**](https://nodejs.org/en/) 17 | * [**Express**](https://expressjs.com/) 18 | * [**MySQL**](https://www.mysql.com/downloads/) 19 | 20 | ### Installation 21 | 22 | * Clone this repository 23 | 24 | * Navigate to the project directory 25 | 26 | * Run `npm install` or `yarn` to instal the projects dependencies 27 | * create a `.env` file and copy the contents of the `.env.sample` file into it and supply the values for each variable 28 | 29 | ```sh 30 | cp .env.sample .env 31 | ``` 32 | * Create a MySQL database and run the `sql` file in the database directory to migrate the database 33 | 34 | ```sh 35 | mysql -u -D -p < ./src/database/database.sql 36 | ``` 37 | 38 | * Run `npm run dev` to start the app in development 39 | 40 | ## Docker 41 | 42 | * Build image 43 | 44 | `docker build -t node_challenge .` 45 | 46 | * Run container 47 | `docker run --rm -p 8000:80 node_challenge` 48 | 49 | ## Request and Response Object API guide for all Endpoints 50 | Check [here](https://docs.google.com/document/d/1J12z1vPo8S5VEmcHGNejjJBOcqmPrr6RSQNdL58qJyE/edit?usp=sharing) -------------------------------------------------------------------------------- /database/dump.sql: -------------------------------------------------------------------------------- 1 | -- Create tshirtshop tables 2 | 3 | -- Create department table 4 | CREATE TABLE `department` ( 5 | `department_id` INT NOT NULL AUTO_INCREMENT, 6 | `name` VARCHAR(100) NOT NULL, 7 | `description` VARCHAR(1000), 8 | PRIMARY KEY (`department_id`) 9 | ) ENGINE=MyISAM; 10 | 11 | -- Create category table 12 | CREATE TABLE `category` ( 13 | `category_id` INT NOT NULL AUTO_INCREMENT, 14 | `department_id` INT NOT NULL, 15 | `name` VARCHAR(100) NOT NULL, 16 | `description` VARCHAR(1000), 17 | PRIMARY KEY (`category_id`), 18 | KEY `idx_category_department_id` (`department_id`) 19 | ) ENGINE=MyISAM; 20 | 21 | -- Create product table 22 | CREATE TABLE `product` ( 23 | `product_id` INT NOT NULL AUTO_INCREMENT, 24 | `name` VARCHAR(100) NOT NULL, 25 | `description` VARCHAR(1000) NOT NULL, 26 | `price` DECIMAL(10,2) NOT NULL, 27 | `discounted_price` DECIMAL(10,2) NOT NULL DEFAULT '0.00', 28 | `image` VARCHAR(150), 29 | `image_2` VARCHAR(150), 30 | `thumbnail` VARCHAR(150), 31 | `display` SMALLINT(6) NOT NULL DEFAULT '0', 32 | PRIMARY KEY (`product_id`), 33 | FULLTEXT KEY `idx_ft_product_name_description` (`name`, `description`) 34 | ) ENGINE=MyISAM; 35 | 36 | -- Create product_category table 37 | CREATE TABLE `product_category` ( 38 | `product_id` INT NOT NULL, 39 | `category_id` INT NOT NULL, 40 | PRIMARY KEY (`product_id`, `category_id`) 41 | ) ENGINE=MyISAM; 42 | 43 | -- Create attribute table (stores attributes such as Size and Color) 44 | CREATE TABLE `attribute` ( 45 | `attribute_id` INT NOT NULL AUTO_INCREMENT, 46 | `name` VARCHAR(100) NOT NULL, -- E.g. Color, Size 47 | PRIMARY KEY (`attribute_id`) 48 | ) ENGINE=MyISAM; 49 | 50 | 51 | -- Create attribute_value table (stores values such as Yellow or XXL) 52 | CREATE TABLE `attribute_value` ( 53 | `attribute_value_id` INT NOT NULL AUTO_INCREMENT, 54 | `attribute_id` INT NOT NULL, -- The ID of the attribute 55 | `value` VARCHAR(100) NOT NULL, -- E.g. Yellow 56 | PRIMARY KEY (`attribute_value_id`), 57 | KEY `idx_attribute_value_attribute_id` (`attribute_id`) 58 | ) ENGINE=MyISAM; 59 | 60 | -- Create product_attribute table (associates attribute values to products) 61 | CREATE TABLE `product_attribute` ( 62 | `product_id` INT NOT NULL, 63 | `attribute_value_id` INT NOT NULL, 64 | PRIMARY KEY (`product_id`, `attribute_value_id`) 65 | ) ENGINE=MyISAM; 66 | 67 | 68 | -- Create shopping_cart table 69 | CREATE TABLE `shopping_cart` ( 70 | `item_id` INT NOT NULL AUTO_INCREMENT, 71 | `cart_id` CHAR(32) NOT NULL, 72 | `product_id` INT NOT NULL, 73 | `attributes` VARCHAR(1000) NOT NULL, 74 | `quantity` INT NOT NULL, 75 | `buy_now` BOOL NOT NULL DEFAULT true, 76 | `added_on` DATETIME NOT NULL, 77 | PRIMARY KEY (`item_id`), 78 | KEY `idx_shopping_cart_cart_id` (`cart_id`) 79 | ) ENGINE=MyISAM; 80 | 81 | -- Create orders table 82 | CREATE TABLE `orders` ( 83 | `order_id` INT NOT NULL AUTO_INCREMENT, 84 | `total_amount` DECIMAL(10,2) NOT NULL DEFAULT '0.00', 85 | `created_on` DATETIME NOT NULL, 86 | `shipped_on` DATETIME, 87 | `status` INT NOT NULL DEFAULT '0', 88 | `comments` VARCHAR(255), 89 | `customer_id` INT, 90 | `auth_code` VARCHAR(50), 91 | `reference` VARCHAR(50), 92 | `shipping_id` INT, 93 | `tax_id` INT, 94 | PRIMARY KEY (`order_id`), 95 | KEY `idx_orders_customer_id` (`customer_id`), 96 | KEY `idx_orders_shipping_id` (`shipping_id`), 97 | KEY `idx_orders_tax_id` (`tax_id`) 98 | ) ENGINE=MyISAM; 99 | 100 | -- Create order_details table 101 | CREATE TABLE `order_detail` ( 102 | `item_id` INT NOT NULL AUTO_INCREMENT, 103 | `order_id` INT NOT NULL, 104 | `product_id` INT NOT NULL, 105 | `attributes` VARCHAR(1000) NOT NULL, 106 | `product_name` VARCHAR(100) NOT NULL, 107 | `quantity` INT NOT NULL, 108 | `unit_cost` DECIMAL(10,2) NOT NULL, 109 | PRIMARY KEY (`item_id`), 110 | KEY `idx_order_detail_order_id` (`order_id`) 111 | ) ENGINE=MyISAM; 112 | 113 | -- Create shipping_region table 114 | CREATE TABLE `shipping_region` ( 115 | `shipping_region_id` INT NOT NULL AUTO_INCREMENT, 116 | `shipping_region` VARCHAR(100) NOT NULL, 117 | PRIMARY KEY (`shipping_region_id`) 118 | ) ENGINE=MyISAM; 119 | 120 | -- Create customer table 121 | CREATE TABLE `customer` ( 122 | `customer_id` INT NOT NULL AUTO_INCREMENT, 123 | `name` VARCHAR(50) NOT NULL, 124 | `email` VARCHAR(100) NOT NULL, 125 | `password` VARCHAR(100) NOT NULL, 126 | `credit_card` TEXT, 127 | `address_1` VARCHAR(100), 128 | `address_2` VARCHAR(100), 129 | `city` VARCHAR(100), 130 | `region` VARCHAR(100), 131 | `postal_code` VARCHAR(100), 132 | `country` VARCHAR(100), 133 | `shipping_region_id` INT NOT NULL default '1', 134 | `day_phone` varchar(100), 135 | `eve_phone` varchar(100), 136 | `mob_phone` varchar(100), 137 | PRIMARY KEY (`customer_id`), 138 | UNIQUE KEY `idx_customer_email` (`email`), 139 | KEY `idx_customer_shipping_region_id` (`shipping_region_id`) 140 | ) ENGINE=MyISAM; 141 | 142 | -- Create shipping table 143 | CREATE TABLE `shipping` ( 144 | `shipping_id` INT NOT NULL AUTO_INCREMENT, 145 | `shipping_type` VARCHAR(100) NOT NULL, 146 | `shipping_cost` NUMERIC(10, 2) NOT NULL, 147 | `shipping_region_id` INT NOT NULL, 148 | PRIMARY KEY (`shipping_id`), 149 | KEY `idx_shipping_shipping_region_id` (`shipping_region_id`) 150 | ) ENGINE=MyISAM; 151 | 152 | -- Create tax table 153 | CREATE TABLE `tax` ( 154 | `tax_id` INT NOT NULL AUTO_INCREMENT, 155 | `tax_type` VARCHAR(100) NOT NULL, 156 | `tax_percentage` NUMERIC(10, 2) NOT NULL, 157 | PRIMARY KEY (`tax_id`) 158 | ) ENGINE=MyISAM; 159 | 160 | -- Create audit table 161 | CREATE TABLE `audit` ( 162 | `audit_id` INT NOT NULL AUTO_INCREMENT, 163 | `order_id` INT NOT NULL, 164 | `created_on` DATETIME NOT NULL, 165 | `message` TEXT NOT NULL, 166 | `code` INT NOT NULL, 167 | PRIMARY KEY (`audit_id`), 168 | KEY `idx_audit_order_id` (`order_id`) 169 | ) ENGINE=MyISAM; 170 | 171 | -- Create review table 172 | CREATE TABLE `review` ( 173 | `review_id` INT NOT NULL AUTO_INCREMENT, 174 | `customer_id` INT NOT NULL, 175 | `product_id` INT NOT NULL, 176 | `review` TEXT NOT NULL, 177 | `rating` SMALLINT NOT NULL, 178 | `created_on` DATETIME NOT NULL, 179 | PRIMARY KEY (`review_id`), 180 | KEY `idx_review_customer_id` (`customer_id`), 181 | KEY `idx_review_product_id` (`product_id`) 182 | ) ENGINE=MyISAM; 183 | 184 | -- Populate department table 185 | INSERT INTO `department` (`department_id`, `name`, `description`) VALUES 186 | (1, 'Regional', 'Proud of your country? Wear a T-shirt with a national symbol stamp!'), 187 | (2, 'Nature', 'Find beautiful T-shirts with animals and flowers in our Nature department!'), 188 | (3, 'Seasonal', 'Each time of the year has a special flavor. Our seasonal T-shirts express traditional symbols using unique postal stamp pictures.'); 189 | 190 | -- Populate category table 191 | INSERT INTO `category` (`category_id`, `department_id`, `name`, `description`) VALUES 192 | (1, 1, 'French', 'The French have always had an eye for beauty. One look at the T-shirts below and you''ll see that same appreciation has been applied abundantly to their postage stamps. Below are some of our most beautiful and colorful T-shirts, so browse away! And don''t forget to go all the way to the bottom - you don''t want to miss any of them!'), 193 | (2, 1, 'Italian', 'The full and resplendent treasure chest of art, literature, music, and science that Italy has given the world is reflected splendidly in its postal stamps. If we could, we would dedicate hundreds of T-shirts to this amazing treasure of beautiful images, but for now we will have to live with what you see here. You don''t have to be Italian to love these gorgeous T-shirts, just someone who appreciates the finer things in life!'), 194 | (3, 1, 'Irish', 'It was Churchill who remarked that he thought the Irish most curious because they didn''t want to be English. How right he was! But then, he was half-American, wasn''t he? If you have an Irish genealogy you will want these T-shirts! If you suddenly turn Irish on St. Patrick''s Day, you too will want these T-shirts! Take a look at some of the coolest T-shirts we have!'), 195 | (4, 2, 'Animal', ' Our ever-growing selection of beautiful animal T-shirts represents critters from everywhere, both wild and domestic. If you don''t see the T-shirt with the animal you''re looking for, tell us and we''ll find it!'), 196 | (5, 2, 'Flower', 'These unique and beautiful flower T-shirts are just the item for the gardener, flower arranger, florist, or general lover of things beautiful. Surprise the flower in your life with one of the beautiful botanical T-shirts or just get a few for yourself!'), 197 | (6, 3, 'Christmas', ' Because this is a unique Christmas T-shirt that you''ll only wear a few times a year, it will probably last for decades (unless some grinch nabs it from you, of course). Far into the future, after you''re gone, your grandkids will pull it out and argue over who gets to wear it. What great snapshots they''ll make dressed in Grandpa or Grandma''s incredibly tasteful and unique Christmas T-shirt! Yes, everyone will remember you forever and what a silly goof you were when you would wear only your Santa beard and cap so you wouldn''t cover up your nifty T-shirt.'), 198 | (7, 3, 'Valentine''s', 'For the more timid, all you have to do is wear your heartfelt message to get it across. Buy one for you and your sweetie(s) today!'); 199 | 200 | -- Populate product table 201 | INSERT INTO `product` (`product_id`, `name`, `description`, `price`, `discounted_price`, `image`, `image_2`, `thumbnail`, `display`) VALUES 202 | (1, 'Arc d''Triomphe', 'This beautiful and iconic T-shirt will no doubt lead you to your own triumph.', 14.99, 0.00, 'arc-d-triomphe.gif', 'arc-d-triomphe-2.gif', 'arc-d-triomphe-thumbnail.gif', 0), 203 | (2, 'Chartres Cathedral', '"The Fur Merchants". Not all the beautiful stained glass in the great cathedrals depicts saints and angels! Lay aside your furs for the summer and wear this beautiful T-shirt!', 16.95, 15.95, 'chartres-cathedral.gif', 'chartres-cathedral-2.gif', 'chartres-cathedral-thumbnail.gif', 2), 204 | (3, 'Coat of Arms', 'There''s good reason why the ship plays a prominent part on this shield!', 14.50, 0.00, 'coat-of-arms.gif', 'coat-of-arms-2.gif', 'coat-of-arms-thumbnail.gif', 0), 205 | (4, 'Gallic Cock', 'This fancy chicken is perhaps the most beloved of all French symbols. Unfortunately, there are only a few hundred left, so you''d better get your T-shirt now!', 18.99, 16.99, 'gallic-cock.gif', 'gallic-cock-2.gif', 'gallic-cock-thumbnail.gif', 2), 206 | (5, 'Marianne', 'She symbolizes the "Triumph of the Republic" and has been depicted many different ways in the history of France, as you will see below!', 15.95, 14.95, 'marianne.gif', 'marianne-2.gif', 'marianne-thumbnail.gif', 2), 207 | (6, 'Alsace', 'It was in this region of France that Gutenberg perfected his movable type. If he could only see what he started!', 16.50, 0.00, 'alsace.gif', 'alsace-2.gif', 'alsace-thumbnail.gif', 0), 208 | (7, 'Apocalypse Tapestry', 'One of the most famous tapestries of the Loire Valley, it dates from the 14th century. The T-shirt is of more recent vintage, however.', 20.00, 18.95, 'apocalypse-tapestry.gif', 'apocalypse-tapestry-2.gif', 'apocalypse-tapestry-thumbnail.gif', 0), 209 | (8, 'Centaur', 'There were never any lady centaurs, so these guys had to mate with nymphs and mares. No wonder they were often in such bad moods!', 14.99, 0.00, 'centaur.gif', 'centaur-2.gif', 'centaur-thumbnail.gif', 0), 210 | (9, 'Corsica', 'Borrowed from Spain, the "Moor''s head" may have celebrated the Christians'' victory over the Moslems in that country.', 22.00, 0.00, 'corsica.gif', 'corsica-2.gif', 'corsica-thumbnail.gif', 0), 211 | (10, 'Haute Couture', 'This stamp publicized the dress making industry. Use it to celebrate the T-shirt industry!', 15.99, 14.95, 'haute-couture.gif', 'haute-couture-2.gif', 'haute-couture-thumbnail.gif', 3), 212 | (11, 'Iris', 'Iris was the Goddess of the Rainbow, daughter of the Titans Thaumas and Electra. Are you up to this T-shirt?!', 17.50, 0.00, 'iris.gif', 'iris-2.gif', 'iris-thumbnail.gif', 0), 213 | (12, 'Lorraine', 'The largest American cemetery in France is located in Lorraine and most of the folks there still appreciate that fact.', 16.95, 0.00, 'lorraine.gif', 'lorraine-2.gif', 'lorraine-thumbnail.gif', 0), 214 | (13, 'Mercury', 'Besides being the messenger of the gods, did you know that Mercury was also the god of profit and commerce? This T-shirt is for business owners!', 21.99, 18.95, 'mercury.gif', 'mercury-2.gif', 'mercury-thumbnail.gif', 2), 215 | (14, 'County of Nice', 'Nice is so nice that it has been fought over for millennia, but now it all belongs to France.', 12.95, 0.00, 'county-of-nice.gif', 'county-of-nice-2.gif', 'county-of-nice-thumbnail.gif', 0), 216 | (15, 'Notre Dame', 'Commemorating the 800th anniversary of the famed cathedral.', 18.50, 16.99, 'notre-dame.gif', 'notre-dame-2.gif', 'notre-dame-thumbnail.gif', 2), 217 | (16, 'Paris Peace Conference', 'The resulting treaties allowed Italy, Romania, Hungary, Bulgaria, and Finland to reassume their responsibilities as sovereign states in international affairs and thus qualify for membership in the UN.', 16.95, 15.99, 'paris-peace-conference.gif', 'paris-peace-conference-2.gif', 'paris-peace-conference-thumbnail.gif', 2), 218 | (17, 'Sarah Bernhardt', 'The "Divine Sarah" said this about Americans: "You are younger than we as a race, you are perhaps barbaric, but what of it? You are still in the molding. Your spirit is superb. It is what helped us win the war." Perhaps we''re still barbaric but we''re still winning wars for them too!', 14.99, 0.00, 'sarah-bernhardt.gif', 'sarah-bernhardt-2.gif', 'sarah-bernhardt-thumbnail.gif', 0), 219 | (18, 'Hunt', 'A scene from "Les Tres Riches Heures," a medieval "book of hours" containing the text for each liturgical hour of the day. This scene is from a 14th century painting.', 16.99, 15.95, 'hunt.gif', 'hunt-2.gif', 'hunt-thumbnail.gif', 2), 220 | (19, 'Italia', 'The War had just ended when this stamp was designed, and even so, there was enough optimism to show the destroyed oak tree sprouting again from its stump! What a beautiful T-shirt!', 22.00, 18.99, 'italia.gif', 'italia-2.gif', 'italia-thumbnail.gif', 2), 221 | (20, 'Torch', 'The light goes on! Carry the torch with this T-shirt and be a beacon of hope for the world!', 19.99, 17.95, 'torch.gif', 'torch-2.gif', 'torch-thumbnail.gif', 2), 222 | (21, 'Espresso', 'The winged foot of Mercury speeds the Special Delivery mail to its destination. In a hurry? This T-shirt is for you!', 16.95, 0.00, 'espresso.gif', 'espresso-2.gif', 'espresso-thumbnail.gif', 0), 223 | (22, 'Galileo', 'This beautiful T-shirt does honor to one of Italy''s (and the world''s) most famous scientists. Show your appreciation for the education you''ve received!', 14.99, 0.00, 'galileo.gif', 'galileo-2.gif', 'galileo-thumbnail.gif', 0), 224 | (23, 'Italian Airmail', 'Thanks to modern Italian post, folks were able to reach out and touch each other. Or at least so implies this image. This is a very fast and friendly T-shirt--you''ll make friends with it!', 21.00, 17.99, 'italian-airmail.gif', 'italian-airmail-2.gif', 'italian-airmail-thumbnail.gif', 0), 225 | (24, 'Mazzini', 'Giuseppe Mazzini is considered one of the patron saints of the "Risorgimiento." Wear this beautiful T-shirt to tell the world you agree!', 20.50, 18.95, 'mazzini.gif', 'mazzini-2.gif', 'mazzini-thumbnail.gif', 2), 226 | (25, 'Romulus & Remus', 'Back in 753 BC, so the story goes, Romulus founded the city of Rome (in competition with Remus, who founded a city on another hill). Their adopted mother is shown in this image. When did they suspect they were adopted?', 17.99, 16.95, 'romulus-remus.gif', 'romulus-remus-2.gif', 'romulus-remus-thumbnail.gif', 2), 227 | (26, 'Italy Maria', 'This beautiful image of the Virgin is from a work by Raphael, whose life and death it honors. It is one of our most popular T-shirts!', 14.00, 0.00, 'italy-maria.gif', 'italy-maria-2.gif', 'italy-maria-thumbnail.gif', 0), 228 | (27, 'Italy Jesus', 'This image of Jesus teaching the gospel was issued to commemorate the third centenary of the "propagation of the faith." Now you can do your part with this T-shirt!', 16.95, 0.00, 'italy-jesus.gif', 'italy-jesus-2.gif', 'italy-jesus-thumbnail.gif', 0), 229 | (28, 'St. Francis', 'Here St. Francis is receiving his vision. This dramatic and attractive stamp was issued on the 700th anniversary of that event.', 22.00, 18.99, 'st-francis.gif', 'st-francis-2.gif', 'st-francis-thumbnail.gif', 2), 230 | (29, 'Irish Coat of Arms', 'This was one of the first stamps of the new Irish Republic, and it makes a T-shirt you''ll be proud to wear on St. Paddy''s Day!', 14.99, 0.00, 'irish-coat-of-arms.gif', 'irish-coat-of-arms-2.gif', 'irish-coat-of-arms-thumbnail.gif', 0), 231 | (30, 'Easter Rebellion', 'The Easter Rebellion of 1916 was a defining moment in Irish history. Although only a few hundred participated and the British squashed it in a week, its leaders were executed, which galvanized the uncommitted.', 19.00, 16.95, 'easter-rebellion.gif', 'easter-rebellion-2.gif', 'easter-rebellion-thumbnail.gif', 2), 232 | (31, 'Guiness', 'Class! Who is this man and why is he important enough for his own T-shirt?!', 15.00, 0.00, 'guiness.gif', 'guiness-2.gif', 'guiness-thumbnail.gif', 0), 233 | (32, 'St. Patrick', 'This stamp commemorated the 1500th anniversary of the revered saint''s death. Is there a more perfect St. Patrick''s Day T-shirt?!', 20.50, 17.95, 'st-patrick.gif', 'st-patrick-2.gif', 'st-patrick-thumbnail.gif', 0), 234 | (33, 'St. Peter', 'This T-shirt commemorates the holy year of 1950.', 16.00, 14.95, 'st-peter.gif', 'st-peter-2.gif', 'st-peter-thumbnail.gif', 2), 235 | (34, 'Sword of Light', 'This was the very first Irish postage stamp, and what a beautiful and cool T-shirt it makes for the Irish person in your life!', 14.99, 0.00, 'sword-of-light.gif', 'sword-of-light-2.gif', 'sword-of-light-thumbnail.gif', 0), 236 | (35, 'Thomas Moore', 'One of the greatest if not the greatest of Irish poets and writers, Moore led a very interesting life, though plagued with tragedy in a somewhat typically Irish way. Remember "The Last Rose of Summer"?', 15.95, 14.99, 'thomas-moore.gif', 'thomas-moore-2.gif', 'thomas-moore-thumbnail.gif', 2), 237 | (36, 'Visit the Zoo', 'This WPA poster is a wonderful example of the art produced by the Works Projects Administration during the Depression years. Do you feel like you sometimes live or work in a zoo? Then this T-shirt is for you!', 20.00, 16.95, 'visit-the-zoo.gif', 'visit-the-zoo-2.gif', 'visit-the-zoo-thumbnail.gif', 2), 238 | (37, 'Sambar', 'This handsome Malayan Sambar was a pain in the neck to get to pose like this, and all so you could have this beautiful retro animal T-shirt!', 19.00, 17.99, 'sambar.gif', 'sambar-2.gif', 'sambar-thumbnail.gif', 2), 239 | (38, 'Buffalo', 'Of all the critters in our T-shirt zoo, this is one of our most popular. A classic animal T-shirt for an individual like yourself!', 14.99, 0.00, 'buffalo.gif', 'buffalo-2.gif', 'buffalo-thumbnail.gif', 0), 240 | (39, 'Mustache Monkey', 'This fellow is more than equipped to hang out with that tail of his, just like you''ll be fit for hanging out with this great animal T-shirt!', 20.00, 17.95, 'mustache-monkey.gif', 'mustache-monkey-2.gif', 'mustache-monkey-thumbnail.gif', 2), 241 | (40, 'Colobus', 'Why is he called "Colobus," "the mutilated one"? He doesn''t have a thumb, just four fingers! He is far from handicapped, however; his hands make him the great swinger he is. Speaking of swinging, that''s what you''ll do with this beautiful animal T-shirt!', 17.00, 15.99, 'colobus.gif', 'colobus-2.gif', 'colobus-thumbnail.gif', 2), 242 | (41, 'Canada Goose', 'Being on a major flyway for these guys, we know all about these majestic birds. They hang out in large numbers on a lake near our house and fly over constantly. Remember what Frankie Lane said? "I want to go where the wild goose goes!" And when you go, wear this cool Canada goose animal T-shirt.', 15.99, 0.00, 'canada-goose.gif', 'canada-goose-2.gif', 'canada-goose-thumbnail.gif', 0), 243 | (42, 'Congo Rhino', 'Among land mammals, this white rhino is surpassed in size only by the elephant. He has a big fan base too, working hard to make sure he sticks around. You''ll be a fan of his, too, when people admire this unique and beautiful T-shirt on you!', 20.00, 18.99, 'congo-rhino.gif', 'congo-rhino-2.gif', 'congo-rhino-thumbnail.gif', 2), 244 | (43, 'Equatorial Rhino', 'There''s a lot going on in this frame! A black rhino is checking out that python slithering off into the bush--or is he eyeing you? You can bet all eyes will be on you when you wear this T-shirt!', 19.95, 17.95, 'equatorial-rhino.gif', 'equatorial-rhino-2.gif', 'equatorial-rhino-thumbnail.gif', 2), 245 | (44, 'Ethiopian Rhino', 'Another white rhino is honored in this classic design that bespeaks the Africa of the early century. This pointillist and retro T-shirt will definitely turn heads!', 16.00, 0.00, 'ethiopian-rhino.gif', 'ethiopian-rhino-2.gif', 'ethiopian-rhino-thumbnail.gif', 0), 246 | (45, 'Dutch Sea Horse', 'I think this T-shirt is destined to be one of our most popular simply because it is one of our most beautiful!', 12.50, 0.00, 'dutch-sea-horse.gif', 'dutch-sea-horse-2.gif', 'dutch-sea-horse-thumbnail.gif', 0), 247 | (46, 'Dutch Swans', 'This stamp was designed in the middle of the Nazi occupation, as was the one above. Together they reflect a spirit of beauty that evil could not suppress. Both of these T-shirts will make it impossible to suppress your artistic soul, too!', 21.00, 18.99, 'dutch-swans.gif', 'dutch-swans-2.gif', 'dutch-swans-thumbnail.gif', 2), 248 | (47, 'Ethiopian Elephant', 'From the same series as the Ethiopian Rhino and the Ostriches, this stylish elephant T-shirt will mark you as a connoisseur of good taste!', 18.99, 16.95, 'ethiopian-elephant.gif', 'ethiopian-elephant-2.gif', 'ethiopian-elephant-thumbnail.gif', 2), 249 | (48, 'Laotian Elephant', 'This working guy is proud to have his own stamp, and now he has his own T-shirt!', 21.00, 18.99, 'laotian-elephant.gif', 'laotian-elephant-2.gif', 'laotian-elephant-thumbnail.gif', 0), 250 | (49, 'Liberian Elephant', 'And yet another Jumbo! You need nothing but a big heart to wear this T-shirt (or a big sense of style)!', 22.00, 17.50, 'liberian-elephant.gif', 'liberian-elephant-2.gif', 'liberian-elephant-thumbnail.gif', 2), 251 | (50, 'Somali Ostriches', 'Another in an old series of beautiful stamps from Ethiopia. These big birds pack quite a wallop, and so will you when you wear this uniquely retro T-shirt!', 12.95, 0.00, 'somali-ostriches.gif', 'somali-ostriches-2.gif', 'somali-ostriches-thumbnail.gif', 0), 252 | (51, 'Tankanyika Giraffe', 'The photographer had to stand on a step ladder for this handsome portrait, but his efforts paid off with an angle we seldom see of this lofty creature. This beautiful retro T-shirt would make him proud!', 15.00, 12.99, 'tankanyika-giraffe.gif', 'tankanyika-giraffe-2.gif', 'tankanyika-giraffe-thumbnail.gif', 3), 253 | (52, 'Ifni Fish', 'This beautiful stamp was issued to commemorate National Colonial Stamp Day (you can do that when you have a colony). When you wear this fancy fish T-shirt, your friends will think it''s national T-shirt day!', 14.00, 0.00, 'ifni-fish.gif', 'ifni-fish-2.gif', 'ifni-fish-thumbnail.gif', 0), 254 | (53, 'Sea Gull', 'A beautiful stamp from a small enclave in southern Morocco that belonged to Spain until 1969 makes a beautiful bird T-shirt.', 19.00, 16.95, 'sea-gull.gif', 'sea-gull-2.gif', 'sea-gull-thumbnail.gif', 2), 255 | (54, 'King Salmon', 'You can fish them and eat them and now you can wear them with this classic animal T-shirt.', 17.95, 15.99, 'king-salmon.gif', 'king-salmon-2.gif', 'king-salmon-thumbnail.gif', 2), 256 | (55, 'Laos Bird', 'This fellow is also known as the "White Crested Laughing Thrush." What''s he laughing at? Why, at the joy of being on your T-shirt!', 12.00, 0.00, 'laos-bird.gif', 'laos-bird-2.gif', 'laos-bird-thumbnail.gif', 0), 257 | (56, 'Mozambique Lion', 'The Portuguese were too busy to run this colony themselves so they gave the Mozambique Company a charter to do it. I think there must be some pretty curious history related to that (the charter only lasted for 50 years)! If you''re a Leo, or know a Leo, you should seriously consider this T-shirt!', 15.99, 14.95, 'mozambique-lion.gif', 'mozambique-lion-2.gif', 'mozambique-lion-thumbnail.gif', 2), 258 | (57, 'Peru Llama', 'This image is nearly 100 years old! Little did this little llama realize that he was going to be made immortal on the Web and on this very unique animal T-shirt (actually, little did he know at all)!', 21.50, 17.99, 'peru-llama.gif', 'peru-llama-2.gif', 'peru-llama-thumbnail.gif', 2), 259 | (58, 'Romania Alsatian', 'If you know and love this breed, there''s no reason in the world that you shouldn''t buy this T-shirt right now!', 15.95, 0.00, 'romania-alsatian.gif', 'romania-alsatian-2.gif', 'romania-alsatian-thumbnail.gif', 0), 260 | (59, 'Somali Fish', 'This is our most popular fish T-shirt, hands down. It''s a beauty, and if you wear this T-shirt, you''ll be letting the world know you''re a fine catch!', 19.95, 16.95, 'somali-fish.gif', 'somali-fish-2.gif', 'somali-fish-thumbnail.gif', 2), 261 | (60, 'Trout', 'This beautiful image will warm the heart of any fisherman! You must know one if you''re not one yourself, so you must buy this T-shirt!', 14.00, 0.00, 'trout.gif', 'trout-2.gif', 'trout-thumbnail.gif', 0), 262 | (61, 'Baby Seal', 'Ahhhhhh! This little harp seal would really prefer not to be your coat! But he would like to be your T-shirt!', 21.00, 18.99, 'baby-seal.gif', 'baby-seal-2.gif', 'baby-seal-thumbnail.gif', 2), 263 | (62, 'Musk Ox', 'Some critters you just don''t want to fool with, and if I were facing this fellow I''d politely give him the trail! That is, of course, unless I were wearing this T-shirt.', 15.50, 0.00, 'musk-ox.gif', 'musk-ox-2.gif', 'musk-ox-thumbnail.gif', 0), 264 | (63, 'Suvla Bay', ' In 1915, Newfoundland sent its Newfoundland Regiment to Suvla Bay in Gallipoli to fight the Turks. This classic image does them honor. Have you ever heard of them? Share the news with this great T-shirt!', 12.99, 0.00, 'suvla-bay.gif', 'suvla-bay-2.gif', 'suvla-bay-thumbnail.gif', 0), 265 | (64, 'Caribou', 'There was a time when Newfoundland was a self-governing dominion of the British Empire, so it printed its own postage. The themes are as typically Canadian as can be, however, as shown by this "King of the Wilde" T-shirt!', 21.00, 19.95, 'caribou.gif', 'caribou-2.gif', 'caribou-thumbnail.gif', 2), 266 | (65, 'Afghan Flower', 'This beautiful image was issued to celebrate National Teachers Day. Perhaps you know a teacher who would love this T-shirt?', 18.50, 16.99, 'afghan-flower.gif', 'afghan-flower-2.gif', 'afghan-flower-thumbnail.gif', 2), 267 | (66, 'Albania Flower', 'Well, these crab apples started out as flowers, so that''s close enough for us! They still make for a uniquely beautiful T-shirt.', 16.00, 14.95, 'albania-flower.gif', 'albania-flower-2.gif', 'albania-flower-thumbnail.gif', 2), 268 | (67, 'Austria Flower', 'Have you ever had nasturtiums on your salad? Try it--they''re almost as good as having them on your T-shirt!', 12.99, 0.00, 'austria-flower.gif', 'austria-flower-2.gif', 'austria-flower-thumbnail.gif', 0), 269 | (68, 'Bulgarian Flower', 'For your interest (and to impress your friends), this beautiful stamp was issued to honor the George Dimitrov state printing works. You''ll need to know this when you wear the T-shirt.', 16.00, 14.99, 'bulgarian-flower.gif', 'bulgarian-flower-2.gif', 'bulgarian-flower-thumbnail.gif', 2), 270 | (69, 'Colombia Flower', 'Celebrating the 75th anniversary of the Universal Postal Union, a date to mark on your calendar and on which to wear this T-shirt!', 14.50, 12.95, 'colombia-flower.gif', 'colombia-flower-2.gif', 'colombia-flower-thumbnail.gif', 1), 271 | (70, 'Congo Flower', 'The Congo is not at a loss for beautiful flowers, and we''ve picked a few of them for your T-shirts.', 21.00, 17.99, 'congo-flower.gif', 'congo-flower-2.gif', 'congo-flower-thumbnail.gif', 2), 272 | (71, 'Costa Rica Flower', 'This national flower of Costa Rica is one of our most beloved flower T-shirts (you can see one on Jill, above). You will surely stand out in this T-shirt!', 12.99, 0.00, 'costa-rica-flower.gif', 'costa-rica-flower.gif', 'costa-rica-flower-thumbnail.gif', 0), 273 | (72, 'Gabon Flower', 'The combretum, also known as "jungle weed," is used in China as a cure for opium addiction. Unfortunately, when you wear this T-shirt, others may become hopelessly addicted to you!', 19.00, 16.95, 'gabon-flower.gif', 'gabon-flower-2.gif', 'gabon-flower-thumbnail.gif', 2), 274 | (73, 'Ghana Flower', 'This is one of the first gingers to bloom in the spring--just like you when you wear this T-shirt!', 21.00, 18.99, 'ghana-flower.gif', 'ghana-flower-2.gif', 'ghana-flower-thumbnail.gif', 2), 275 | (74, 'Israel Flower', 'This plant is native to the rocky and sandy regions of the western United States, so when you come across one, it really stands out. And so will you when you put on this beautiful T-shirt!', 19.50, 17.50, 'israel-flower.gif', 'israel-flower-2.gif', 'israel-flower-thumbnail.gif', 2), 276 | (75, 'Poland Flower', 'A beautiful and sunny T-shirt for both spring and summer!', 16.95, 15.99, 'poland-flower.gif', 'poland-flower-2.gif', 'poland-flower-thumbnail.gif', 2), 277 | (76, 'Romania Flower', 'Also known as the spring pheasant''s eye, this flower belongs on your T-shirt this summer to help you catch a few eyes.', 12.95, 0.00, 'romania-flower.gif', 'romania-flower-2.gif', 'romania-flower-thumbnail.gif', 0), 278 | (77, 'Russia Flower', 'Someone out there who can speak Russian needs to tell me what this plant is. I''ll sell you the T-shirt for $10 if you can!', 21.00, 18.95, 'russia-flower.gif', 'russia-flower-2.gif', 'russia-flower-thumbnail.gif', 0), 279 | (78, 'San Marino Flower', '"A white sport coat and a pink carnation, I''m all dressed up for the dance!" Well, how about a white T-shirt and a pink carnation?!', 19.95, 17.99, 'san-marino-flower.gif', 'san-marino-flower-2.gif', 'san-marino-flower-thumbnail.gif', 2), 280 | (79, 'Uruguay Flower', 'The Indian Queen Anahi was the ugliest woman ever seen. But instead of living a slave when captured by the Conquistadores, she immolated herself in a fire and was reborn the most beautiful of flowers: the ceibo, national flower of Uruguay. Of course, you won''t need to burn to wear this T-shirt, but you may cause some pretty hot glances to be thrown your way!', 17.99, 16.99, 'uruguay-flower.gif', 'uruguay-flower-2.gif', 'uruguay-flower-thumbnail.gif', 2), 281 | (80, 'Snow Deer', 'Tarmo has produced some wonderful Christmas T-shirts for us, and we hope to have many more soon.', 21.00, 18.95, 'snow-deer.gif', 'snow-deer-2.gif', 'snow-deer-thumbnail.gif', 2), 282 | (81, 'Holly Cat', 'Few things make a cat happier at Christmas than a tree suddenly appearing in the house!', 15.99, 0.00, 'holly-cat.gif', 'holly-cat-2.gif', 'holly-cat-thumbnail.gif', 0), 283 | (82, 'Christmas Seal', 'Is this your grandmother? It could be, you know, and I''d bet she''d recognize the Christmas seal on this cool Christmas T-shirt.', 19.99, 17.99, 'christmas-seal.gif', 'christmas-seal-2.gif', 'christmas-seal-thumbnail.gif', 2), 284 | (83, 'Weather Vane', 'This weather vane dates from the 1830''s and is still showing which way the wind blows! Trumpet your arrival with this unique Christmas T-shirt.', 15.95, 14.99, 'weather-vane.gif', 'weather-vane-2.gif', 'weather-vane-thumbnail.gif', 2), 285 | (84, 'Mistletoe', 'This well-known parasite and killer of trees was revered by the Druids, who would go out and gather it with great ceremony. Youths would go about with it to announce the new year. Eventually more engaging customs were attached to the strange plant, and we''re here to see that they continue with these cool Christmas T-shirts.', 19.00, 17.99, 'mistletoe.gif', 'mistletoe-2.gif', 'mistletoe-thumbnail.gif', 3), 286 | (85, 'Altar Piece', 'This beautiful angel Christmas T-shirt is awaiting the opportunity to adorn your chest!', 20.50, 18.50, 'altar-piece.gif', 'altar-piece-2.gif', 'altar-piece-thumbnail.gif', 2), 287 | (86, 'The Three Wise Men', 'This is a classic rendition of one of the season’s most beloved stories, and now showing on a Christmas T-shirt for you!', 12.99, 0.00, 'the-three-wise-men.gif', 'the-three-wise-men-2.gif', 'the-three-wise-men-thumbnail.gif', 0), 288 | (87, 'Christmas Tree', 'Can you get more warm and folksy than this classic Christmas T-shirt?', 20.00, 17.95, 'christmas-tree.gif', 'christmas-tree-2.gif', 'christmas-tree-thumbnail.gif', 2), 289 | (88, 'Madonna & Child', 'This exquisite image was painted by Filipino Lippi, a 15th century Italian artist. I think he would approve of it on a Going Postal Christmas T-shirt!', 21.95, 18.50, 'madonna-child.gif', 'madonna-child-2.gif', 'madonna-child-thumbnail.gif', 0), 290 | (89, 'The Virgin Mary', 'This stained glass window is found in Glasgow Cathedral, Scotland, and was created by Gabriel Loire of France, one of the most prolific of artists in this medium--and now you can have it on this wonderful Christmas T-shirt.', 16.95, 15.95, 'the-virgin-mary.gif', 'the-virgin-mary-2.gif', 'the-virgin-mary-thumbnail.gif', 2), 291 | (90, 'Adoration of the Kings', 'This design is from a miniature in the Evangelistary of Matilda in Nonantola Abbey, from the 12th century. As a Christmas T-shirt, it will cause you to be adored!', 17.50, 16.50, 'adoration-of-the-kings.gif', 'adoration-of-the-kings-2.gif', 'adoration-of-the-kings-thumbnail.gif', 2), 292 | (91, 'A Partridge in a Pear Tree', 'The original of this beautiful stamp is by Jamie Wyeth and is in the National Gallery of Art. The next best is on our beautiful Christmas T-shirt!', 14.99, 0.00, 'a-partridge-in-a-pear-tree.gif', 'a-partridge-in-a-pear-tree-2.gif', 'a-partridge-in-a-pear-tree-thumbnail.gif', 0), 293 | (92, 'St. Lucy', 'This is a tiny detail of a large work called "Mary, Queen of Heaven," done in 1480 by a Flemish master known only as "The Master of St. Lucy Legend." The original is in a Bruges church. The not-quite-original is on this cool Christmas T-shirt.', 18.95, 0.00, 'st-lucy.gif', 'st-lucy-2.gif', 'st-lucy-thumbnail.gif', 0), 294 | (93, 'St. Lucia', 'Saint Lucia''s tradition is an important part of Swedish Christmas, and an important part of that are the candles. Next to the candles in importance is this popular Christmas T-shirt!', 19.00, 17.95, 'st-lucia.gif', 'st-lucia-2.gif', 'st-lucia-thumbnail.gif', 2), 295 | (94, 'Swede Santa', 'Santa as a child. You must know a child who would love this cool Christmas T-shirt!?', 21.00, 18.50, 'swede-santa.gif', 'swede-santa-2.gif', 'swede-santa-thumbnail.gif', 2), 296 | (95, 'Wreath', 'Hey! I''ve got an idea! Why not buy two of these cool Christmas T-shirts so you can wear one and tack the other one to your door?!', 18.99, 16.99, 'wreath.gif', 'wreath-2.gif', 'wreath-thumbnail.gif', 2), 297 | (96, 'Love', 'Here''s a Valentine''s day T-shirt that will let you say it all in just one easy glance--there''s no mistake about it!', 19.00, 17.50, 'love.gif', 'love-2.gif', 'love-thumbnail.gif', 2), 298 | (97, 'Birds', 'Is your heart all aflutter? Show it with this T-shirt!', 21.00, 18.95, 'birds.gif', 'birds-2.gif', 'birds-thumbnail.gif', 2), 299 | (98, 'Kat Over New Moon', 'Love making you feel lighthearted?', 14.99, 0.00, 'kat-over-new-moon.gif', 'kat-over-new-moon-2.gif', 'kat-over-new-moon-thumbnail.gif', 0), 300 | (99, 'Thrilling Love', 'This girl''s got her hockey hunk right where she wants him!', 21.00, 18.50, 'thrilling-love.gif', 'thrilling-love-2.gif', 'thrilling-love-thumbnail.gif', 2), 301 | (100, 'The Rapture of Psyche', 'Now we''re getting a bit more serious!', 18.95, 16.99, 'the-rapture-of-psyche.gif', 'the-rapture-of-psyche-2.gif', 'the-rapture-of-psyche-thumbnail.gif', 2), 302 | (101, 'The Promise of Spring', 'With Valentine''s Day come, can Spring be far behind?', 21.00, 19.50, 'the-promise-of-spring.gif', 'the-promise-of-spring-2.gif', 'the-promise-of-spring-thumbnail.gif', 0); 303 | 304 | -- Populate product_category table 305 | INSERT INTO `product_category` (`product_id`, `category_id`) VALUES 306 | (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), (9, 1), 307 | (10, 1), (11, 1), (12, 1), (13, 1), (14, 1), (15, 1), (16, 1), (17, 1), 308 | (18, 1), (19, 2), (20, 2), (21, 2), (22, 2), (23, 2), (24, 2), (25, 2), 309 | (26, 2), (27, 2), (28, 2), (29, 3), (30, 3), (31, 3), (32, 3), (33, 3), 310 | (34, 3), (35, 3), (36, 4), (37, 4), (38, 4), (39, 4), (40, 4), (41, 4), 311 | (42, 4), (43, 4), (44, 4), (45, 4), (46, 4), (47, 4), (48, 4), (49, 4), 312 | (50, 4), (51, 4), (52, 4), (53, 4), (54, 4), (55, 4), (56, 4), (57, 4), 313 | (58, 4), (59, 4), (60, 4), (61, 4), (62, 4), (63, 4), (64, 4), (81, 4), 314 | (97, 4), (98, 4), (65, 5), (66, 5), (67, 5), (68, 5), (69, 5), (70, 5), 315 | (71, 5), (72, 5), (73, 5), (74, 5), (75, 5), (76, 5), (77, 5), (78, 5), 316 | (79, 5), (80, 6), (81, 6), (82, 6), (83, 6), (84, 6), (85, 6), (86, 6), 317 | (87, 6), (88, 6), (89, 6), (90, 6), (91, 6), (92, 6), (93, 6), (94, 6), 318 | (95, 6), (96, 7), (97, 7), (98, 7), (99, 7), (100, 7), (101, 7); 319 | 320 | -- Populate attribute table 321 | INSERT INTO `attribute` (`attribute_id`, `name`) VALUES 322 | (1, 'Size'), (2, 'Color'); 323 | 324 | -- Populate attribute_value table 325 | INSERT INTO `attribute_value` (`attribute_value_id`, `attribute_id`, `value`) VALUES 326 | (1, 1, 'S'), (2, 1, 'M'), (3, 1, 'L'), (4, 1, 'XL'), (5, 1, 'XXL'), 327 | (6, 2, 'White'), (7, 2, 'Black'), (8, 2, 'Red'), (9, 2, 'Orange'), 328 | (10, 2, 'Yellow'), (11, 2, 'Green'), (12, 2, 'Blue'), 329 | (13, 2, 'Indigo'), (14, 2, 'Purple'); 330 | 331 | -- Populate product_attribute table 332 | INSERT INTO `product_attribute` (`product_id`, `attribute_value_id`) 333 | SELECT `p`.`product_id`, `av`.`attribute_value_id` 334 | FROM `product` `p`, `attribute_value` `av`; 335 | 336 | -- Populate shipping_region table 337 | INSERT INTO `shipping_region` (`shipping_region_id`, `shipping_region`) VALUES 338 | (1, 'Please Select') , (2, 'US / Canada'), 339 | (3, 'Europe'), (4, 'Rest of World'); 340 | 341 | -- Populate shipping table 342 | INSERT INTO `shipping` (`shipping_id`, `shipping_type`, 343 | `shipping_cost`, `shipping_region_id`) VALUES 344 | (1, 'Next Day Delivery ($20)', 20.00, 2), 345 | (2, '3-4 Days ($10)', 10.00, 2), 346 | (3, '7 Days ($5)', 5.00, 2), 347 | (4, 'By air (7 days, $25)', 25.00, 3), 348 | (5, 'By sea (28 days, $10)', 10.00, 3), 349 | (6, 'By air (10 days, $35)', 35.00, 4), 350 | (7, 'By sea (28 days, $30)', 30.00, 4); 351 | 352 | -- Populate tax table 353 | INSERT INTO `tax` (`tax_id`, `tax_type`, `tax_percentage`) VALUES 354 | (1, 'Sales Tax at 8.5%', 8.50), 355 | (2, 'No Tax', 0.00); 356 | 357 | -- Change DELIMITER to $$ 358 | DELIMITER $$ 359 | 360 | -- Create catalog_get_departments_list stored procedure 361 | CREATE PROCEDURE catalog_get_departments_list() 362 | BEGIN 363 | SELECT department_id, name FROM department ORDER BY department_id; 364 | END$$ 365 | 366 | -- Create catalog_get_department_details stored procedure 367 | CREATE PROCEDURE catalog_get_department_details(IN inDepartmentId INT) 368 | BEGIN 369 | SELECT name, description 370 | FROM department 371 | WHERE department_id = inDepartmentId; 372 | END$$ 373 | 374 | -- Create catalog_get_categories_list stored procedure 375 | CREATE PROCEDURE catalog_get_categories_list(IN inDepartmentId INT) 376 | BEGIN 377 | SELECT category_id, name 378 | FROM category 379 | WHERE department_id = inDepartmentId 380 | ORDER BY category_id; 381 | END$$ 382 | 383 | -- Create catalog_get_category_details stored procedure 384 | CREATE PROCEDURE catalog_get_category_details(IN inCategoryId INT) 385 | BEGIN 386 | SELECT name, description 387 | FROM category 388 | WHERE category_id = inCategoryId; 389 | END$$ 390 | 391 | -- Create catalog_count_products_in_category stored procedure 392 | CREATE PROCEDURE catalog_count_products_in_category(IN inCategoryId INT) 393 | BEGIN 394 | SELECT COUNT(*) AS categories_count 395 | FROM product p 396 | INNER JOIN product_category pc 397 | ON p.product_id = pc.product_id 398 | WHERE pc.category_id = inCategoryId; 399 | END$$ 400 | 401 | -- Create catalog_get_products_in_category stored procedure 402 | CREATE PROCEDURE catalog_get_products_in_category( 403 | IN inCategoryId INT, IN inShortProductDescriptionLength INT, 404 | IN inProductsPerPage INT, IN inStartItem INT) 405 | BEGIN 406 | -- Prepare statement 407 | PREPARE statement FROM 408 | "SELECT p.product_id, p.name, 409 | IF(LENGTH(p.description) <= ?, 410 | p.description, 411 | CONCAT(LEFT(p.description, ?), 412 | '...')) AS description, 413 | p.price, p.discounted_price, p.thumbnail 414 | FROM product p 415 | INNER JOIN product_category pc 416 | ON p.product_id = pc.product_id 417 | WHERE pc.category_id = ? 418 | ORDER BY p.display DESC 419 | LIMIT ?, ?"; 420 | 421 | -- Define query parameters 422 | SET @p1 = inShortProductDescriptionLength; 423 | SET @p2 = inShortProductDescriptionLength; 424 | SET @p3 = inCategoryId; 425 | SET @p4 = inStartItem; 426 | SET @p5 = inProductsPerPage; 427 | 428 | -- Execute the statement 429 | EXECUTE statement USING @p1, @p2, @p3, @p4, @p5; 430 | END$$ 431 | 432 | -- Create catalog_count_products_on_department stored procedure 433 | CREATE PROCEDURE catalog_count_products_on_department(IN inDepartmentId INT) 434 | BEGIN 435 | SELECT DISTINCT COUNT(*) AS products_on_department_count 436 | FROM product p 437 | INNER JOIN product_category pc 438 | ON p.product_id = pc.product_id 439 | INNER JOIN category c 440 | ON pc.category_id = c.category_id 441 | WHERE (p.display = 2 OR p.display = 3) 442 | AND c.department_id = inDepartmentId; 443 | END$$ 444 | 445 | -- Create catalog_get_products_on_department stored procedure 446 | CREATE PROCEDURE catalog_get_products_on_department( 447 | IN inDepartmentId INT, IN inShortProductDescriptionLength INT, 448 | IN inProductsPerPage INT, IN inStartItem INT) 449 | BEGIN 450 | PREPARE statement FROM 451 | "SELECT DISTINCT p.product_id, p.name, 452 | IF(LENGTH(p.description) <= ?, 453 | p.description, 454 | CONCAT(LEFT(p.description, ?), 455 | '...')) AS description, 456 | p.price, p.discounted_price, p.thumbnail 457 | FROM product p 458 | INNER JOIN product_category pc 459 | ON p.product_id = pc.product_id 460 | INNER JOIN category c 461 | ON pc.category_id = c.category_id 462 | WHERE (p.display = 2 OR p.display = 3) 463 | AND c.department_id = ? 464 | ORDER BY p.display DESC 465 | LIMIT ?, ?"; 466 | 467 | SET @p1 = inShortProductDescriptionLength; 468 | SET @p2 = inShortProductDescriptionLength; 469 | SET @p3 = inDepartmentId; 470 | SET @p4 = inStartItem; 471 | SET @p5 = inProductsPerPage; 472 | 473 | EXECUTE statement USING @p1, @p2, @p3, @p4, @p5; 474 | END$$ 475 | 476 | -- Create catalog_count_products_on_catalog stored procedure 477 | CREATE PROCEDURE catalog_count_products_on_catalog() 478 | BEGIN 479 | SELECT COUNT(*) AS products_on_catalog_count 480 | FROM product 481 | WHERE display = 1 OR display = 3; 482 | END$$ 483 | 484 | -- Create catalog_get_products_on_catalog stored procedure 485 | CREATE PROCEDURE catalog_get_products_on_catalog( 486 | IN inShortProductDescriptionLength INT, 487 | IN inProductsPerPage INT, IN inStartItem INT) 488 | BEGIN 489 | PREPARE statement FROM 490 | "SELECT product_id, name, 491 | IF(LENGTH(description) <= ?, 492 | description, 493 | CONCAT(LEFT(description, ?), 494 | '...')) AS description, 495 | price, discounted_price, thumbnail 496 | FROM product 497 | WHERE display = 1 OR display = 3 498 | ORDER BY display DESC 499 | LIMIT ?, ?"; 500 | 501 | SET @p1 = inShortProductDescriptionLength; 502 | SET @p2 = inShortProductDescriptionLength; 503 | SET @p3 = inStartItem; 504 | SET @p4 = inProductsPerPage; 505 | 506 | EXECUTE statement USING @p1, @p2, @p3, @p4; 507 | END$$ 508 | 509 | -- Create catalog_get_product_details stored procedure 510 | CREATE PROCEDURE catalog_get_product_details(IN inProductId INT) 511 | BEGIN 512 | SELECT product_id, name, description, 513 | price, discounted_price, image, image_2 514 | FROM product 515 | WHERE product_id = inProductId; 516 | END$$ 517 | 518 | -- Create catalog_get_product_locations stored procedure 519 | CREATE PROCEDURE catalog_get_product_locations(IN inProductId INT) 520 | BEGIN 521 | SELECT c.category_id, c.name AS category_name, c.department_id, 522 | (SELECT name 523 | FROM department 524 | WHERE department_id = c.department_id) AS department_name 525 | -- Subquery returns the name of the department of the category 526 | FROM category c 527 | WHERE c.category_id IN 528 | (SELECT category_id 529 | FROM product_category 530 | WHERE product_id = inProductId); 531 | -- Subquery returns the category IDs a product belongs to 532 | END$$ 533 | 534 | -- Create catalog_get_product_attributes stored procedure 535 | CREATE PROCEDURE catalog_get_product_attributes(IN inProductId INT) 536 | BEGIN 537 | SELECT a.name AS attribute_name, 538 | av.attribute_value_id, av.value AS attribute_value 539 | FROM attribute_value av 540 | INNER JOIN attribute a 541 | ON av.attribute_id = a.attribute_id 542 | WHERE av.attribute_value_id IN 543 | (SELECT attribute_value_id 544 | FROM product_attribute 545 | WHERE product_id = inProductId) 546 | ORDER BY a.name; 547 | END$$ 548 | 549 | -- Create catalog_get_department_name stored procedure 550 | CREATE PROCEDURE catalog_get_department_name(IN inDepartmentId INT) 551 | BEGIN 552 | SELECT name FROM department WHERE department_id = inDepartmentId; 553 | END$$ 554 | 555 | -- Create catalog_get_category_name stored procedure 556 | CREATE PROCEDURE catalog_get_category_name(IN inCategoryId INT) 557 | BEGIN 558 | SELECT name FROM category WHERE category_id = inCategoryId; 559 | END$$ 560 | 561 | -- Create catalog_get_product_name stored procedure 562 | CREATE PROCEDURE catalog_get_product_name(IN inProductId INT) 563 | BEGIN 564 | SELECT name FROM product WHERE product_id = inProductId; 565 | END$$ 566 | 567 | -- Create catalog_count_search_result stored procedure 568 | CREATE PROCEDURE catalog_count_search_result( 569 | IN inSearchString TEXT, IN inAllWords VARCHAR(3)) 570 | BEGIN 571 | IF inAllWords = "on" THEN 572 | PREPARE statement FROM 573 | "SELECT count(*) 574 | FROM product 575 | WHERE MATCH (name, description) AGAINST (? IN BOOLEAN MODE)"; 576 | ELSE 577 | PREPARE statement FROM 578 | "SELECT count(*) 579 | FROM product 580 | WHERE MATCH (name, description) AGAINST (?)"; 581 | END IF; 582 | 583 | SET @p1 = inSearchString; 584 | 585 | EXECUTE statement USING @p1; 586 | END$$ 587 | 588 | -- Create catalog_search stored procedure 589 | CREATE PROCEDURE catalog_search( 590 | IN inSearchString TEXT, IN inAllWords VARCHAR(3), 591 | IN inShortProductDescriptionLength INT, 592 | IN inProductsPerPage INT, IN inStartItem INT) 593 | BEGIN 594 | IF inAllWords = "on" THEN 595 | PREPARE statement FROM 596 | "SELECT product_id, name, 597 | IF(LENGTH(description) <= ?, 598 | description, 599 | CONCAT(LEFT(description, ?), 600 | '...')) AS description, 601 | price, discounted_price, thumbnail 602 | FROM product 603 | WHERE MATCH (name, description) 604 | AGAINST (? IN BOOLEAN MODE) 605 | ORDER BY MATCH (name, description) 606 | AGAINST (? IN BOOLEAN MODE) DESC 607 | LIMIT ?, ?"; 608 | ELSE 609 | PREPARE statement FROM 610 | "SELECT product_id, name, 611 | IF(LENGTH(description) <= ?, 612 | description, 613 | CONCAT(LEFT(description, ?), 614 | '...')) AS description, 615 | price, discounted_price, thumbnail 616 | FROM product 617 | WHERE MATCH (name, description) AGAINST (?) 618 | ORDER BY MATCH (name, description) AGAINST (?) DESC 619 | LIMIT ?, ?"; 620 | END IF; 621 | 622 | SET @p1 = inShortProductDescriptionLength; 623 | SET @p2 = inSearchString; 624 | SET @p3 = inStartItem; 625 | SET @p4 = inProductsPerPage; 626 | 627 | EXECUTE statement USING @p1, @p1, @p2, @p2, @p3, @p4; 628 | END$$ 629 | 630 | -- Create catalog_get_departments stored procedure 631 | CREATE PROCEDURE catalog_get_departments() 632 | BEGIN 633 | SELECT department_id, name, description 634 | FROM department 635 | ORDER BY department_id; 636 | END$$ 637 | 638 | -- Create catalog_add_department stored procedure 639 | CREATE PROCEDURE catalog_add_department( 640 | IN inName VARCHAR(100), IN inDescription VARCHAR(1000)) 641 | BEGIN 642 | INSERT INTO department (name, description) 643 | VALUES (inName, inDescription); 644 | END$$ 645 | 646 | -- Create catalog_update_department stored procedure 647 | CREATE PROCEDURE catalog_update_department(IN inDepartmentId INT, 648 | IN inName VARCHAR(100), IN inDescription VARCHAR(1000)) 649 | BEGIN 650 | UPDATE department 651 | SET name = inName, description = inDescription 652 | WHERE department_id = inDepartmentId; 653 | END$$ 654 | 655 | -- Create catalog_delete_department stored procedure 656 | CREATE PROCEDURE catalog_delete_department(IN inDepartmentId INT) 657 | BEGIN 658 | DECLARE categoryRowsCount INT; 659 | 660 | SELECT count(*) 661 | FROM category 662 | WHERE department_id = inDepartmentId 663 | INTO categoryRowsCount; 664 | 665 | IF categoryRowsCount = 0 THEN 666 | DELETE FROM department WHERE department_id = inDepartmentId; 667 | 668 | SELECT 1; 669 | ELSE 670 | SELECT -1; 671 | END IF; 672 | END$$ 673 | 674 | -- Create catalog_get_department_categories stored procedure 675 | CREATE PROCEDURE catalog_get_department_categories(IN inDepartmentId INT) 676 | BEGIN 677 | SELECT category_id, name, description 678 | FROM category 679 | WHERE department_id = inDepartmentId 680 | ORDER BY category_id; 681 | END$$ 682 | 683 | -- Create catalog_add_category stored procedure 684 | CREATE PROCEDURE catalog_add_category(IN inDepartmentId INT, 685 | IN inName VARCHAR(100), IN inDescription VARCHAR(1000)) 686 | BEGIN 687 | INSERT INTO category (department_id, name, description) 688 | VALUES (inDepartmentId, inName, inDescription); 689 | END$$ 690 | 691 | -- Create catalog_update_category stored procedure 692 | CREATE PROCEDURE catalog_update_category(IN inCategoryId INT, 693 | IN inName VARCHAR(100), IN inDescription VARCHAR(1000)) 694 | BEGIN 695 | UPDATE category 696 | SET name = inName, description = inDescription 697 | WHERE category_id = inCategoryId; 698 | END$$ 699 | 700 | -- Create catalog_delete_category stored procedure 701 | CREATE PROCEDURE catalog_delete_category(IN inCategoryId INT) 702 | BEGIN 703 | DECLARE productCategoryRowsCount INT; 704 | 705 | SELECT count(*) 706 | FROM product p 707 | INNER JOIN product_category pc 708 | ON p.product_id = pc.product_id 709 | WHERE pc.category_id = inCategoryId 710 | INTO productCategoryRowsCount; 711 | 712 | IF productCategoryRowsCount = 0 THEN 713 | DELETE FROM category WHERE category_id = inCategoryId; 714 | 715 | SELECT 1; 716 | ELSE 717 | SELECT -1; 718 | END IF; 719 | END$$ 720 | 721 | -- Create catalog_get_attributes stored procedure 722 | CREATE PROCEDURE catalog_get_attributes() 723 | BEGIN 724 | SELECT attribute_id, name FROM attribute ORDER BY attribute_id; 725 | END$$ 726 | 727 | -- Create catalog_add_attribute stored procedure 728 | CREATE PROCEDURE catalog_add_attribute(IN inName VARCHAR(100)) 729 | BEGIN 730 | INSERT INTO attribute (name) VALUES (inName); 731 | END$$ 732 | 733 | -- Create catalog_update_attribute stored procedure 734 | CREATE PROCEDURE catalog_update_attribute( 735 | IN inAttributeId INT, IN inName VARCHAR(100)) 736 | BEGIN 737 | UPDATE attribute SET name = inName WHERE attribute_id = inAttributeId; 738 | END$$ 739 | 740 | -- Create catalog_delete_attribute stored procedure 741 | CREATE PROCEDURE catalog_delete_attribute(IN inAttributeId INT) 742 | BEGIN 743 | DECLARE attributeRowsCount INT; 744 | 745 | SELECT count(*) 746 | FROM attribute_value 747 | WHERE attribute_id = inAttributeId 748 | INTO attributeRowsCount; 749 | 750 | IF attributeRowsCount = 0 THEN 751 | DELETE FROM attribute WHERE attribute_id = inAttributeId; 752 | 753 | SELECT 1; 754 | ELSE 755 | SELECT -1; 756 | END IF; 757 | END$$ 758 | 759 | -- Create catalog_get_attribute_details stored procedure 760 | CREATE PROCEDURE catalog_get_attribute_details(IN inAttributeId INT) 761 | BEGIN 762 | SELECT attribute_id, name 763 | FROM attribute 764 | WHERE attribute_id = inAttributeId; 765 | END$$ 766 | 767 | -- Create catalog_get_attribute_values stored procedure 768 | CREATE PROCEDURE catalog_get_attribute_values(IN inAttributeId INT) 769 | BEGIN 770 | SELECT attribute_value_id, value 771 | FROM attribute_value 772 | WHERE attribute_id = inAttributeId 773 | ORDER BY attribute_id; 774 | END$$ 775 | 776 | -- Create catalog_add_attribute_value stored procedure 777 | CREATE PROCEDURE catalog_add_attribute_value( 778 | IN inAttributeId INT, IN inValue VARCHAR(100)) 779 | BEGIN 780 | INSERT INTO attribute_value (attribute_id, value) 781 | VALUES (inAttributeId, inValue); 782 | END$$ 783 | 784 | -- Create catalog_update_attribute_value stored procedure 785 | CREATE PROCEDURE catalog_update_attribute_value( 786 | IN inAttributeValueId INT, IN inValue VARCHAR(100)) 787 | BEGIN 788 | UPDATE attribute_value 789 | SET value = inValue 790 | WHERE attribute_value_id = inAttributeValueId; 791 | END$$ 792 | 793 | -- Create catalog_delete_attribute_value stored procedure 794 | CREATE PROCEDURE catalog_delete_attribute_value(IN inAttributeValueId INT) 795 | BEGIN 796 | DECLARE productAttributeRowsCount INT; 797 | 798 | SELECT count(*) 799 | FROM product p 800 | INNER JOIN product_attribute pa 801 | ON p.product_id = pa.product_id 802 | WHERE pa.attribute_value_id = inAttributeValueId 803 | INTO productAttributeRowsCount; 804 | 805 | IF productAttributeRowsCount = 0 THEN 806 | DELETE FROM attribute_value WHERE attribute_value_id = inAttributeValueId; 807 | 808 | SELECT 1; 809 | ELSE 810 | SELECT -1; 811 | END IF; 812 | END$$ 813 | 814 | -- Create catalog_get_category_products stored procedure 815 | CREATE PROCEDURE catalog_get_category_products(IN inCategoryId INT) 816 | BEGIN 817 | SELECT p.product_id, p.name, p.description, p.price, 818 | p.discounted_price 819 | FROM product p 820 | INNER JOIN product_category pc 821 | ON p.product_id = pc.product_id 822 | WHERE pc.category_id = inCategoryId 823 | ORDER BY p.product_id; 824 | END$$ 825 | 826 | -- Create catalog_add_product_to_category stored procedure 827 | CREATE PROCEDURE catalog_add_product_to_category(IN inCategoryId INT, 828 | IN inName VARCHAR(100), IN inDescription VARCHAR(1000), 829 | IN inPrice DECIMAL(10, 2)) 830 | BEGIN 831 | DECLARE productLastInsertId INT; 832 | 833 | INSERT INTO product (name, description, price) 834 | VALUES (inName, inDescription, inPrice); 835 | 836 | SELECT LAST_INSERT_ID() INTO productLastInsertId; 837 | 838 | INSERT INTO product_category (product_id, category_id) 839 | VALUES (productLastInsertId, inCategoryId); 840 | END$$ 841 | 842 | -- Create catalog_update_product stored procedure 843 | CREATE PROCEDURE catalog_update_product(IN inProductId INT, 844 | IN inName VARCHAR(100), IN inDescription VARCHAR(1000), 845 | IN inPrice DECIMAL(10, 2), IN inDiscountedPrice DECIMAL(10, 2)) 846 | BEGIN 847 | UPDATE product 848 | SET name = inName, description = inDescription, price = inPrice, 849 | discounted_price = inDiscountedPrice 850 | WHERE product_id = inProductId; 851 | END$$ 852 | 853 | -- Create catalog_remove_product_from_category stored procedure 854 | CREATE PROCEDURE catalog_remove_product_from_category( 855 | IN inProductId INT, IN inCategoryId INT) 856 | BEGIN 857 | DECLARE productCategoryRowsCount INT; 858 | 859 | SELECT count(*) 860 | FROM product_category 861 | WHERE product_id = inProductId 862 | INTO productCategoryRowsCount; 863 | 864 | IF productCategoryRowsCount = 1 THEN 865 | CALL catalog_delete_product(inProductId); 866 | 867 | SELECT 0; 868 | ELSE 869 | DELETE FROM product_category 870 | WHERE category_id = inCategoryId AND product_id = inProductId; 871 | 872 | SELECT 1; 873 | END IF; 874 | END$$ 875 | 876 | -- Create catalog_get_categories stored procedure 877 | CREATE PROCEDURE catalog_get_categories() 878 | BEGIN 879 | SELECT category_id, name, description 880 | FROM category 881 | ORDER BY category_id; 882 | END$$ 883 | 884 | -- Create catalog_get_product_info stored procedure 885 | CREATE PROCEDURE catalog_get_product_info(IN inProductId INT) 886 | BEGIN 887 | SELECT product_id, name, description, price, discounted_price, 888 | image, image_2, thumbnail, display 889 | FROM product 890 | WHERE product_id = inProductId; 891 | END$$ 892 | 893 | -- Create catalog_get_categories_for_product stored procedure 894 | CREATE PROCEDURE catalog_get_categories_for_product(IN inProductId INT) 895 | BEGIN 896 | SELECT c.category_id, c.department_id, c.name 897 | FROM category c 898 | JOIN product_category pc 899 | ON c.category_id = pc.category_id 900 | WHERE pc.product_id = inProductId 901 | ORDER BY category_id; 902 | END$$ 903 | 904 | -- Create catalog_set_product_display_option stored procedure 905 | CREATE PROCEDURE catalog_set_product_display_option( 906 | IN inProductId INT, IN inDisplay SMALLINT) 907 | BEGIN 908 | UPDATE product SET display = inDisplay WHERE product_id = inProductId; 909 | END$$ 910 | 911 | -- Create catalog_assign_product_to_category stored procedure 912 | CREATE PROCEDURE catalog_assign_product_to_category( 913 | IN inProductId INT, IN inCategoryId INT) 914 | BEGIN 915 | INSERT INTO product_category (product_id, category_id) 916 | VALUES (inProductId, inCategoryId); 917 | END$$ 918 | 919 | -- Create catalog_move_product_to_category stored procedure 920 | CREATE PROCEDURE catalog_move_product_to_category(IN inProductId INT, 921 | IN inSourceCategoryId INT, IN inTargetCategoryId INT) 922 | BEGIN 923 | UPDATE product_category 924 | SET category_id = inTargetCategoryId 925 | WHERE product_id = inProductId 926 | AND category_id = inSourceCategoryId; 927 | END$$ 928 | 929 | -- Create catalog_get_attributes_not_assigned_to_product stored procedure 930 | CREATE PROCEDURE catalog_get_attributes_not_assigned_to_product( 931 | IN inProductId INT) 932 | BEGIN 933 | SELECT a.name AS attribute_name, 934 | av.attribute_value_id, av.value AS attribute_value 935 | FROM attribute_value av 936 | INNER JOIN attribute a 937 | ON av.attribute_id = a.attribute_id 938 | WHERE av.attribute_value_id NOT IN 939 | (SELECT attribute_value_id 940 | FROM product_attribute 941 | WHERE product_id = inProductId) 942 | ORDER BY attribute_name, av.attribute_value_id; 943 | END$$ 944 | 945 | -- Create catalog_assign_attribute_value_to_product stored procedure 946 | CREATE PROCEDURE catalog_assign_attribute_value_to_product( 947 | IN inProductId INT, IN inAttributeValueId INT) 948 | BEGIN 949 | INSERT INTO product_attribute (product_id, attribute_value_id) 950 | VALUES (inProductId, inAttributeValueId); 951 | END$$ 952 | 953 | -- Create catalog_remove_product_attribute_value stored procedure 954 | CREATE PROCEDURE catalog_remove_product_attribute_value( 955 | IN inProductId INT, IN inAttributeValueId INT) 956 | BEGIN 957 | DELETE FROM product_attribute 958 | WHERE product_id = inProductId AND 959 | attribute_value_id = inAttributeValueId; 960 | END$$ 961 | 962 | -- Create catalog_set_image stored procedure 963 | CREATE PROCEDURE catalog_set_image( 964 | IN inProductId INT, IN inImage VARCHAR(150)) 965 | BEGIN 966 | UPDATE product SET image = inImage WHERE product_id = inProductId; 967 | END$$ 968 | 969 | -- Create catalog_set_image_2 stored procedure 970 | CREATE PROCEDURE catalog_set_image_2( 971 | IN inProductId INT, IN inImage VARCHAR(150)) 972 | BEGIN 973 | UPDATE product SET image_2 = inImage WHERE product_id = inProductId; 974 | END$$ 975 | 976 | -- Create catalog_set_thumbnail stored procedure 977 | CREATE PROCEDURE catalog_set_thumbnail( 978 | IN inProductId INT, IN inThumbnail VARCHAR(150)) 979 | BEGIN 980 | UPDATE product 981 | SET thumbnail = inThumbnail 982 | WHERE product_id = inProductId; 983 | END$$ 984 | 985 | -- Create shopping_cart_add_product stored procedure 986 | CREATE PROCEDURE shopping_cart_add_product(IN inCartId CHAR(32), 987 | IN inProductId INT, IN inAttributes VARCHAR(1000)) 988 | BEGIN 989 | DECLARE productQuantity INT; 990 | 991 | -- Obtain current shopping cart quantity for the product 992 | SELECT quantity 993 | FROM shopping_cart 994 | WHERE cart_id = inCartId 995 | AND product_id = inProductId 996 | AND attributes = inAttributes 997 | INTO productQuantity; 998 | 999 | -- Create new shopping cart record, or increase quantity of existing record 1000 | IF productQuantity IS NULL THEN 1001 | INSERT INTO shopping_cart(item_id, cart_id, product_id, attributes, 1002 | quantity, added_on) 1003 | VALUES (UUID(), inCartId, inProductId, inAttributes, 1, NOW()); 1004 | ELSE 1005 | UPDATE shopping_cart 1006 | SET quantity = quantity + 1, buy_now = true 1007 | WHERE cart_id = inCartId 1008 | AND product_id = inProductId 1009 | AND attributes = inAttributes; 1010 | END IF; 1011 | END$$ 1012 | 1013 | -- Create shopping_cart_update_product stored procedure 1014 | CREATE PROCEDURE shopping_cart_update(IN inItemId INT, IN inQuantity INT) 1015 | BEGIN 1016 | IF inQuantity > 0 THEN 1017 | UPDATE shopping_cart 1018 | SET quantity = inQuantity, added_on = NOW() 1019 | WHERE item_id = inItemId; 1020 | ELSE 1021 | CALL shopping_cart_remove_product(inItemId); 1022 | END IF; 1023 | END$$ 1024 | 1025 | -- Create shopping_cart_remove_product stored procedure 1026 | CREATE PROCEDURE shopping_cart_remove_product(IN inItemId INT) 1027 | BEGIN 1028 | DELETE FROM shopping_cart WHERE item_id = inItemId; 1029 | END$$ 1030 | 1031 | -- Create shopping_cart_get_products stored procedure 1032 | CREATE PROCEDURE shopping_cart_get_products(IN inCartId CHAR(32)) 1033 | BEGIN 1034 | SELECT sc.item_id, p.name, sc.attributes, 1035 | COALESCE(NULLIF(p.discounted_price, 0), p.price) AS price, 1036 | sc.quantity, 1037 | COALESCE(NULLIF(p.discounted_price, 0), 1038 | p.price) * sc.quantity AS subtotal 1039 | FROM shopping_cart sc 1040 | INNER JOIN product p 1041 | ON sc.product_id = p.product_id 1042 | WHERE sc.cart_id = inCartId AND sc.buy_now; 1043 | END$$ 1044 | 1045 | -- Create shopping_cart_get_saved_products stored procedure 1046 | CREATE PROCEDURE shopping_cart_get_saved_products(IN inCartId CHAR(32)) 1047 | BEGIN 1048 | SELECT sc.item_id, p.name, sc.attributes, 1049 | COALESCE(NULLIF(p.discounted_price, 0), p.price) AS price 1050 | FROM shopping_cart sc 1051 | INNER JOIN product p 1052 | ON sc.product_id = p.product_id 1053 | WHERE sc.cart_id = inCartId AND NOT sc.buy_now; 1054 | END$$ 1055 | 1056 | -- Create shopping_cart_get_total_amount stored procedure 1057 | CREATE PROCEDURE shopping_cart_get_total_amount(IN inCartId CHAR(32)) 1058 | BEGIN 1059 | SELECT SUM(COALESCE(NULLIF(p.discounted_price, 0), p.price) 1060 | * sc.quantity) AS total_amount 1061 | FROM shopping_cart sc 1062 | INNER JOIN product p 1063 | ON sc.product_id = p.product_id 1064 | WHERE sc.cart_id = inCartId AND sc.buy_now; 1065 | END$$ 1066 | 1067 | -- Create shopping_cart_save_product_for_later stored procedure 1068 | CREATE PROCEDURE shopping_cart_save_product_for_later(IN inItemId INT) 1069 | BEGIN 1070 | UPDATE shopping_cart 1071 | SET buy_now = false, quantity = 1 1072 | WHERE item_id = inItemId; 1073 | END$$ 1074 | 1075 | -- Create shopping_cart_move_product_to_cart stored procedure 1076 | CREATE PROCEDURE shopping_cart_move_product_to_cart(IN inItemId INT) 1077 | BEGIN 1078 | UPDATE shopping_cart 1079 | SET buy_now = true, added_on = NOW() 1080 | WHERE item_id = inItemId; 1081 | END$$ 1082 | 1083 | -- Create catalog_delete_product stored procedure 1084 | CREATE PROCEDURE catalog_delete_product(IN inProductId INT) 1085 | BEGIN 1086 | DELETE FROM product_attribute WHERE product_id = inProductId; 1087 | DELETE FROM product_category WHERE product_id = inProductId; 1088 | DELETE FROM shopping_cart WHERE product_id = inProductId; 1089 | DELETE FROM product WHERE product_id = inProductId; 1090 | END$$ 1091 | 1092 | -- Create shopping_cart_count_old_carts stored procedure 1093 | CREATE PROCEDURE shopping_cart_count_old_carts(IN inDays INT) 1094 | BEGIN 1095 | SELECT COUNT(cart_id) AS old_shopping_carts_count 1096 | FROM (SELECT cart_id 1097 | FROM shopping_cart 1098 | GROUP BY cart_id 1099 | HAVING DATE_SUB(NOW(), INTERVAL inDays DAY) >= MAX(added_on)) 1100 | AS old_carts; 1101 | END$$ 1102 | 1103 | -- Create shopping_cart_delete_old_carts stored procedure 1104 | CREATE PROCEDURE shopping_cart_delete_old_carts(IN inDays INT) 1105 | BEGIN 1106 | DELETE FROM shopping_cart 1107 | WHERE cart_id IN 1108 | (SELECT cart_id 1109 | FROM (SELECT cart_id 1110 | FROM shopping_cart 1111 | GROUP BY cart_id 1112 | HAVING DATE_SUB(NOW(), INTERVAL inDays DAY) >= 1113 | MAX(added_on)) 1114 | AS sc); 1115 | END$$ 1116 | 1117 | -- Create shopping_cart_empty stored procedure 1118 | CREATE PROCEDURE shopping_cart_empty(IN inCartId CHAR(32)) 1119 | BEGIN 1120 | DELETE FROM shopping_cart WHERE cart_id = inCartId; 1121 | END$$ 1122 | 1123 | -- Create orders_get_order_details stored procedure 1124 | CREATE PROCEDURE orders_get_order_details(IN inOrderId INT) 1125 | BEGIN 1126 | SELECT order_id, product_id, attributes, product_name, 1127 | quantity, unit_cost, (quantity * unit_cost) AS subtotal 1128 | FROM order_detail 1129 | WHERE order_id = inOrderId; 1130 | END$$ 1131 | 1132 | -- Create catalog_get_recommendations stored procedure 1133 | CREATE PROCEDURE catalog_get_recommendations( 1134 | IN inProductId INT, IN inShortProductDescriptionLength INT) 1135 | BEGIN 1136 | PREPARE statement FROM 1137 | "SELECT od2.product_id, od2.product_name, 1138 | IF(LENGTH(p.description) <= ?, p.description, 1139 | CONCAT(LEFT(p.description, ?), '...')) AS description 1140 | FROM order_detail od1 1141 | JOIN order_detail od2 ON od1.order_id = od2.order_id 1142 | JOIN product p ON od2.product_id = p.product_id 1143 | WHERE od1.product_id = ? AND 1144 | od2.product_id != ? 1145 | GROUP BY od2.product_id 1146 | ORDER BY COUNT(od2.product_id) DESC 1147 | LIMIT 5"; 1148 | 1149 | SET @p1 = inShortProductDescriptionLength; 1150 | SET @p2 = inProductId; 1151 | 1152 | EXECUTE statement USING @p1, @p1, @p2, @p2; 1153 | END$$ 1154 | 1155 | -- Create shopping_cart_get_recommendations stored procedure 1156 | CREATE PROCEDURE shopping_cart_get_recommendations( 1157 | IN inCartId CHAR(32), IN inShortProductDescriptionLength INT) 1158 | BEGIN 1159 | PREPARE statement FROM 1160 | "-- Returns the products that exist in a list of orders 1161 | SELECT od1.product_id, od1.product_name, 1162 | IF(LENGTH(p.description) <= ?, p.description, 1163 | CONCAT(LEFT(p.description, ?), '...')) AS description 1164 | FROM order_detail od1 1165 | JOIN order_detail od2 1166 | ON od1.order_id = od2.order_id 1167 | JOIN product p 1168 | ON od1.product_id = p.product_id 1169 | JOIN shopping_cart 1170 | ON od2.product_id = shopping_cart.product_id 1171 | WHERE shopping_cart.cart_id = ? 1172 | -- Must not include products that already exist 1173 | -- in the visitor's cart 1174 | AND od1.product_id NOT IN 1175 | (-- Returns the products in the specified 1176 | -- shopping cart 1177 | SELECT product_id 1178 | FROM shopping_cart 1179 | WHERE cart_id = ?) 1180 | -- Group the product_id so we can calculate the rank 1181 | GROUP BY od1.product_id 1182 | -- Order descending by rank 1183 | ORDER BY COUNT(od1.product_id) DESC 1184 | LIMIT 5"; 1185 | 1186 | SET @p1 = inShortProductDescriptionLength; 1187 | SET @p2 = inCartId; 1188 | 1189 | EXECUTE statement USING @p1, @p1, @p2, @p2; 1190 | END$$ 1191 | 1192 | -- Create customer_get_login_info stored procedure 1193 | CREATE PROCEDURE customer_get_login_info(IN inEmail VARCHAR(100)) 1194 | BEGIN 1195 | SELECT customer_id, password FROM customer WHERE email = inEmail; 1196 | END$$ 1197 | 1198 | -- Create customer_add stored procedure 1199 | CREATE PROCEDURE customer_add(IN inName VARCHAR(50), 1200 | IN inEmail VARCHAR(100), IN inPassword VARCHAR(50)) 1201 | BEGIN 1202 | INSERT INTO customer (name, email, password) 1203 | VALUES (inName, inEmail, inPassword); 1204 | 1205 | SELECT LAST_INSERT_ID(); 1206 | END$$ 1207 | 1208 | -- Create customer_get_customer stored procedure 1209 | CREATE PROCEDURE customer_get_customer(IN inCustomerId INT) 1210 | BEGIN 1211 | SELECT customer_id, name, email, password, credit_card, 1212 | address_1, address_2, city, region, postal_code, country, 1213 | shipping_region_id, day_phone, eve_phone, mob_phone 1214 | FROM customer 1215 | WHERE customer_id = inCustomerId; 1216 | END$$ 1217 | 1218 | -- Create customer_update_account stored procedure 1219 | CREATE PROCEDURE customer_update_account(IN inCustomerId INT, 1220 | IN inName VARCHAR(50), IN inEmail VARCHAR(100), 1221 | IN inPassword VARCHAR(50), IN inDayPhone VARCHAR(100), 1222 | IN inEvePhone VARCHAR(100), IN inMobPhone VARCHAR(100)) 1223 | BEGIN 1224 | UPDATE customer 1225 | SET name = inName, email = inEmail, 1226 | password = inPassword, day_phone = inDayPhone, 1227 | eve_phone = inEvePhone, mob_phone = inMobPhone 1228 | WHERE customer_id = inCustomerId; 1229 | END$$ 1230 | 1231 | -- Create customer_update_credit_card stored procedure 1232 | CREATE PROCEDURE customer_update_credit_card( 1233 | IN inCustomerId INT, IN inCreditCard TEXT) 1234 | BEGIN 1235 | UPDATE customer 1236 | SET credit_card = inCreditCard 1237 | WHERE customer_id = inCustomerId; 1238 | END$$ 1239 | 1240 | -- Create customer_get_shipping_regions stored procedure 1241 | CREATE PROCEDURE customer_get_shipping_regions() 1242 | BEGIN 1243 | SELECT shipping_region_id, shipping_region FROM shipping_region; 1244 | END$$ 1245 | 1246 | -- Create customer_update_address stored procedure 1247 | CREATE PROCEDURE customer_update_address(IN inCustomerId INT, 1248 | IN inAddress1 VARCHAR(100), IN inAddress2 VARCHAR(100), 1249 | IN inCity VARCHAR(100), IN inRegion VARCHAR(100), 1250 | IN inPostalCode VARCHAR(100), IN inCountry VARCHAR(100), 1251 | IN inShippingRegionId INT) 1252 | BEGIN 1253 | UPDATE customer 1254 | SET address_1 = inAddress1, address_2 = inAddress2, city = inCity, 1255 | region = inRegion, postal_code = inPostalCode, 1256 | country = inCountry, shipping_region_id = inShippingRegionId 1257 | WHERE customer_id = inCustomerId; 1258 | END$$ 1259 | 1260 | -- Create orders_get_most_recent_orders stored procedure 1261 | CREATE PROCEDURE orders_get_most_recent_orders(IN inHowMany INT) 1262 | BEGIN 1263 | PREPARE statement FROM 1264 | "SELECT o.order_id, o.total_amount, o.created_on, 1265 | o.shipped_on, o.status, c.name 1266 | FROM orders o 1267 | INNER JOIN customer c 1268 | ON o.customer_id = c.customer_id 1269 | ORDER BY o.created_on DESC 1270 | LIMIT ?"; 1271 | 1272 | SET @p1 = inHowMany; 1273 | 1274 | EXECUTE statement USING @p1; 1275 | END$$ 1276 | 1277 | -- Create orders_get_orders_between_dates stored procedure 1278 | CREATE PROCEDURE orders_get_orders_between_dates( 1279 | IN inStartDate DATETIME, IN inEndDate DATETIME) 1280 | BEGIN 1281 | SELECT o.order_id, o.total_amount, o.created_on, 1282 | o.shipped_on, o.status, c.name 1283 | FROM orders o 1284 | INNER JOIN customer c 1285 | ON o.customer_id = c.customer_id 1286 | WHERE o.created_on >= inStartDate AND o.created_on <= inEndDate 1287 | ORDER BY o.created_on DESC; 1288 | END$$ 1289 | 1290 | -- Create orders_get_orders_by_status stored procedure 1291 | CREATE PROCEDURE orders_get_orders_by_status(IN inStatus INT) 1292 | BEGIN 1293 | SELECT o.order_id, o.total_amount, o.created_on, 1294 | o.shipped_on, o.status, c.name 1295 | FROM orders o 1296 | INNER JOIN customer c 1297 | ON o.customer_id = c.customer_id 1298 | WHERE o.status = inStatus 1299 | ORDER BY o.created_on DESC; 1300 | END$$ 1301 | 1302 | -- Create orders_get_by_customer_id stored procedure 1303 | CREATE PROCEDURE orders_get_by_customer_id(IN inCustomerId INT) 1304 | BEGIN 1305 | SELECT o.order_id, o.total_amount, o.created_on, 1306 | o.shipped_on, o.status, c.name 1307 | FROM orders o 1308 | INNER JOIN customer c 1309 | ON o.customer_id = c.customer_id 1310 | WHERE o.customer_id = inCustomerId 1311 | ORDER BY o.created_on DESC; 1312 | END$$ 1313 | 1314 | -- Create orders_get_order_short_details stored procedure 1315 | CREATE PROCEDURE orders_get_order_short_details(IN inOrderId INT) 1316 | BEGIN 1317 | SELECT o.order_id, o.total_amount, o.created_on, 1318 | o.shipped_on, o.status, c.name 1319 | FROM orders o 1320 | INNER JOIN customer c 1321 | ON o.customer_id = c.customer_id 1322 | WHERE o.order_id = inOrderId; 1323 | END$$ 1324 | 1325 | -- Create customer_get_customers_list stored procedure 1326 | CREATE PROCEDURE customer_get_customers_list() 1327 | BEGIN 1328 | SELECT customer_id, name FROM customer ORDER BY name ASC; 1329 | END$$ 1330 | 1331 | -- Create shopping_cart_create_order stored procedure 1332 | CREATE PROCEDURE shopping_cart_create_order(IN inCartId CHAR(32), 1333 | IN inCustomerId INT, IN inShippingId INT, IN inTaxId INT) 1334 | BEGIN 1335 | DECLARE orderId INT; 1336 | 1337 | -- Insert a new record into orders and obtain the new order ID 1338 | INSERT INTO orders (created_on, customer_id, shipping_id, tax_id) VALUES 1339 | (NOW(), inCustomerId, inShippingId, inTaxId); 1340 | -- Obtain the new Order ID 1341 | SELECT LAST_INSERT_ID() INTO orderId; 1342 | 1343 | -- Insert order details in order_detail table 1344 | INSERT INTO order_detail (order_id, product_id, attributes, 1345 | product_name, quantity, unit_cost) 1346 | SELECT orderId, p.product_id, sc.attributes, p.name, sc.quantity, 1347 | COALESCE(NULLIF(p.discounted_price, 0), p.price) AS unit_cost 1348 | FROM shopping_cart sc 1349 | INNER JOIN product p 1350 | ON sc.product_id = p.product_id 1351 | WHERE sc.cart_id = inCartId AND sc.buy_now; 1352 | 1353 | -- Save the order's total amount 1354 | UPDATE orders 1355 | SET total_amount = (SELECT SUM(unit_cost * quantity) 1356 | FROM order_detail 1357 | WHERE order_id = orderId) 1358 | WHERE order_id = orderId; 1359 | 1360 | -- Clear the shopping cart 1361 | CALL shopping_cart_empty(inCartId); 1362 | 1363 | -- Return the Order ID 1364 | SELECT orderId; 1365 | END$$ 1366 | 1367 | -- Create orders_get_order_info stored procedure 1368 | CREATE PROCEDURE orders_get_order_info(IN inOrderId INT) 1369 | BEGIN 1370 | SELECT o.order_id, o.total_amount, o.created_on, o.shipped_on, 1371 | o.status, o.comments, o.customer_id, o.auth_code, 1372 | o.reference, o.shipping_id, s.shipping_type, s.shipping_cost, 1373 | o.tax_id, t.tax_type, t.tax_percentage 1374 | FROM orders o 1375 | INNER JOIN tax t 1376 | ON t.tax_id = o.tax_id 1377 | INNER JOIN shipping s 1378 | ON s.shipping_id = o.shipping_id 1379 | WHERE o.order_id = inOrderId; 1380 | END$$ 1381 | 1382 | -- Create orders_get_shipping_info stored procedure 1383 | CREATE PROCEDURE orders_get_shipping_info(IN inShippingRegionId INT) 1384 | BEGIN 1385 | SELECT shipping_id, shipping_type, shipping_cost, shipping_region_id 1386 | FROM shipping 1387 | WHERE shipping_region_id = inShippingRegionId; 1388 | END$$ 1389 | 1390 | -- Create orders_create_audit stored procedure 1391 | CREATE PROCEDURE orders_create_audit(IN inOrderId INT, 1392 | IN inMessage TEXT, IN inCode INT) 1393 | BEGIN 1394 | INSERT INTO audit (order_id, created_on, message, code) 1395 | VALUES (inOrderId, NOW(), inMessage, inCode); 1396 | END$$ 1397 | 1398 | -- Create orders_update_status stored procedure 1399 | CREATE PROCEDURE orders_update_status(IN inOrderId INT, IN inStatus INT) 1400 | BEGIN 1401 | UPDATE orders SET status = inStatus WHERE order_id = inOrderId; 1402 | END$$ 1403 | 1404 | -- Create orders_set_auth_code stored procedure 1405 | CREATE PROCEDURE orders_set_auth_code(IN inOrderId INT, 1406 | IN inAuthCode VARCHAR(50), IN inReference VARCHAR(50)) 1407 | BEGIN 1408 | UPDATE orders 1409 | SET auth_code = inAuthCode, reference = inReference 1410 | WHERE order_id = inOrderId; 1411 | END$$ 1412 | 1413 | -- Create orders_set_date_shipped stored procedure 1414 | CREATE PROCEDURE orders_set_date_shipped(IN inOrderId INT) 1415 | BEGIN 1416 | UPDATE orders SET shipped_on = NOW() WHERE order_id = inOrderId; 1417 | END$$ 1418 | 1419 | -- Create orders_update_order stored procedure 1420 | CREATE PROCEDURE orders_update_order(IN inOrderId INT, IN inStatus INT, 1421 | IN inComments VARCHAR(255), IN inAuthCode VARCHAR(50), 1422 | IN inReference VARCHAR(50)) 1423 | BEGIN 1424 | DECLARE currentDateShipped DATETIME; 1425 | 1426 | SELECT shipped_on 1427 | FROM orders 1428 | WHERE order_id = inOrderId 1429 | INTO currentDateShipped; 1430 | 1431 | UPDATE orders 1432 | SET status = inStatus, comments = inComments, 1433 | auth_code = inAuthCode, reference = inReference 1434 | WHERE order_id = inOrderId; 1435 | 1436 | IF inStatus < 7 AND currentDateShipped IS NOT NULL THEN 1437 | UPDATE orders SET shipped_on = NULL WHERE order_id = inOrderId; 1438 | ELSEIF inStatus > 6 AND currentDateShipped IS NULL THEN 1439 | UPDATE orders SET shipped_on = NOW() WHERE order_id = inOrderId; 1440 | END IF; 1441 | END$$ 1442 | 1443 | -- Create orders_get_audit_trail stored procedure 1444 | CREATE PROCEDURE orders_get_audit_trail(IN inOrderId INT) 1445 | BEGIN 1446 | SELECT audit_id, order_id, created_on, message, code 1447 | FROM audit 1448 | WHERE order_id = inOrderId; 1449 | END$$ 1450 | 1451 | -- Create catalog_get_product_reviews stored procedure 1452 | CREATE PROCEDURE catalog_get_product_reviews(IN inProductId INT) 1453 | BEGIN 1454 | SELECT c.name, r.review, r.rating, r.created_on 1455 | FROM review r 1456 | INNER JOIN customer c 1457 | ON c.customer_id = r.customer_id 1458 | WHERE r.product_id = inProductId 1459 | ORDER BY r.created_on DESC; 1460 | END$$ 1461 | 1462 | -- Create catalog_create_product_review stored procedure 1463 | CREATE PROCEDURE catalog_create_product_review(IN inCustomerId INT, 1464 | IN inProductId INT, IN inReview TEXT, IN inRating SMALLINT) 1465 | BEGIN 1466 | INSERT INTO review (customer_id, product_id, review, rating, created_on) 1467 | VALUES (inCustomerId, inProductId, inReview, inRating, NOW()); 1468 | END$$ 1469 | 1470 | -- Change back DELIMITER to ; 1471 | DELIMITER ; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-shop", 3 | "version": "1.0.0", 4 | "description": "e-commerce application", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon --watch src --exec babel-node ./src/index.js", 8 | "pretest": "NODE_ENV=test babel-node ./src/test/migrate.js", 9 | "test": "jest --no-cache --detectOpenHandles --runInBand --forceExit", 10 | "build": "NODE_ENV=production babel src -d dist --copy-files", 11 | "start": "NODE_ENV=production node dist/index.js", 12 | "test:watch": "jest --no-cache --detectOpenHandles --runInBand --watch" 13 | }, 14 | "keywords": [ 15 | "ecommerce", 16 | "nodejs", 17 | "express", 18 | "caching" 19 | ], 20 | "dependencies": { 21 | "@babel/polyfill": "^7.4.3", 22 | "@sendgrid/mail": "^6.3.1", 23 | "bcrypt": "^3.0.5", 24 | "body-parser": "^1.18.3", 25 | "compression": "^1.7.4", 26 | "cors": "^2.8.5", 27 | "dotenv": "^7.0.0", 28 | "express": "^4.16.4", 29 | "express-validator": "^5.3.1", 30 | "express-winston": "^3.1.0", 31 | "fancy-log": "^1.3.3", 32 | "helmet": "^3.16.0", 33 | "jsonwebtoken": "^8.5.1", 34 | "morgan": "^1.9.1", 35 | "mysql2": "^1.6.5", 36 | "nodemon": "^1.18.11", 37 | "redis": "^2.8.0", 38 | "sequelize": "^5.3.1", 39 | "sequelize-cli": "^5.4.0", 40 | "stripe": "^6.28.0", 41 | "uniqid": "^5.0.3", 42 | "winston": "^3.2.1" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "^7.4.3", 46 | "@babel/core": "^7.4.3", 47 | "@babel/node": "^7.2.2", 48 | "@babel/preset-env": "^7.4.3", 49 | "babel-jest": "^24.7.1", 50 | "chai": "^4.2.0", 51 | "eslint": "^5.3.0", 52 | "eslint-config-airbnb": "^17.1.0", 53 | "eslint-config-prettier": "^4.1.0", 54 | "eslint-plugin-import": "^2.16.0", 55 | "eslint-plugin-jsx-a11y": "^6.2.1", 56 | "eslint-plugin-prettier": "^3.0.1", 57 | "eslint-plugin-react": "^7.12.4", 58 | "jest": "^24.7.1", 59 | "mocha": "^6.1.4", 60 | "prettier": "^1.16.4", 61 | "supertest": "^4.0.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/controllers/__tests__/attributes.controller.test.js: -------------------------------------------------------------------------------- 1 | test('for the truth', () => { 2 | expect(1 + 1).toEqual(2); 3 | }); 4 | -------------------------------------------------------------------------------- /src/controllers/__tests__/customer.controller.test.js: -------------------------------------------------------------------------------- 1 | test('for the truth', () => { 2 | expect(1 + 1).toEqual(2); 3 | }); 4 | -------------------------------------------------------------------------------- /src/controllers/__tests__/product.controller.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import '@babel/polyfill'; 3 | import request from 'supertest'; 4 | 5 | import app, { server } from '../..'; 6 | import { Product, Department } from '../../database/models'; 7 | import truncate from '../../test/helpers'; 8 | 9 | describe('product controller', () => { 10 | let product; 11 | let department; 12 | beforeEach(async done => { 13 | await truncate(); 14 | product = await Product.create({ 15 | name: 'New T shirt', 16 | description: 'Simple T shirt', 17 | price: 14.99, 18 | }); 19 | department = await Department.create({ 20 | name: 'Groceries', 21 | description: 'Daily groceries', 22 | }); 23 | done(); 24 | }); 25 | 26 | afterAll(async done => { 27 | server.close(); 28 | done(); 29 | }); 30 | 31 | describe('getAllProducts', () => { 32 | it('should return a list of products', done => { 33 | request(app) 34 | .get('/products?page=3&search=the&limit=30') 35 | .set('Content-Type', 'application/json') 36 | .end((error, res) => { 37 | expect(res.status).toEqual(200); 38 | expect(typeof res.body).toHaveProperty('rows'); 39 | expect(typeof res.body).toHaveProperty('pagination'); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | describe('getProduct', () => { 46 | it('should get the details of a product', done => { 47 | request(app) 48 | .get(`/products/${product.product_id}`) 49 | .set('Content-Type', 'application/json') 50 | .end((error, res) => { 51 | expect(res.status).toEqual(200); 52 | expect(res.body).toHaveProperty('product_id'); 53 | done(); 54 | }); 55 | }); 56 | 57 | it('should return appropriate status if product is not found', done => { 58 | request(app) 59 | .get('/products/999999') 60 | .set('Content-Type', 'application/json') 61 | .end((error, res) => { 62 | expect(res.status).toEqual(404); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('getAllDepartments', () => { 69 | it('should return a list of departments', done => { 70 | request(app) 71 | .get('/departments') 72 | .set('Content-Type', 'application/json') 73 | .end((error, res) => { 74 | expect(res.status).toEqual(200); 75 | expect(typeof res.body).toEqual('object'); 76 | done(); 77 | }); 78 | }); 79 | }); 80 | 81 | describe('getDepartment', () => { 82 | it('should get the details of a department', done => { 83 | request(app) 84 | .get(`/departments/${department.department_id}`) 85 | .set('Content-Type', 'application/json') 86 | .end((error, res) => { 87 | expect(res.status).toEqual(200); 88 | expect(res.body).toHaveProperty('department_id'); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('should return appropriate status if department is not found', done => { 94 | request(app) 95 | .get('/departments/999999') 96 | .set('Content-Type', 'application/json') 97 | .end((error, res) => { 98 | expect(res.status).toEqual(404); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/controllers/__tests__/shipping.controller.test.js: -------------------------------------------------------------------------------- 1 | test('for the truth', () => { 2 | expect(1 + 1).toEqual(2); 3 | }); 4 | -------------------------------------------------------------------------------- /src/controllers/__tests__/shoppingCart.controller.test.js: -------------------------------------------------------------------------------- 1 | test('for the truth', () => { 2 | expect(1 + 1).toEqual(2); 3 | }); 4 | -------------------------------------------------------------------------------- /src/controllers/__tests__/tax.controller.test.js: -------------------------------------------------------------------------------- 1 | test('for the truth', () => { 2 | expect(1 + 1).toEqual(2); 3 | }); 4 | -------------------------------------------------------------------------------- /src/controllers/attributes.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The controller defined below is the attribute controller, highlighted below are the functions of each static method 3 | * in the controller 4 | * Some methods needs to be implemented from scratch while others may contain one or two bugs 5 | * 6 | * - getAllAttributes - This method should return an array of all attributes 7 | * - getSingleAttribute - This method should return a single attribute using the attribute_id in the request parameter 8 | * - getAttributeValues - This method should return an array of all attribute values of a single attribute using the attribute id 9 | * - getProductAttributes - This method should return an array of all the product attributes 10 | * NB: Check the BACKEND CHALLENGE TEMPLATE DOCUMENTATION in the readme of this repository to see our recommended 11 | * endpoints, request body/param, and response object for each of these method 12 | */ 13 | class AttributeController { 14 | /** 15 | * This method get all attributes 16 | * @param {*} req 17 | * @param {*} res 18 | * @param {*} next 19 | */ 20 | static async getAllAttributes(req, res, next) { 21 | // write code to get all attributes from the database here 22 | return res.status(200).json({ message: 'this works' }); 23 | } 24 | 25 | /** 26 | * This method gets a single attribute using the attribute id 27 | * @param {*} req 28 | * @param {*} res 29 | * @param {*} next 30 | */ 31 | static async getSingleAttribute(req, res, next) { 32 | // Write code to get a single attribute using the attribute id provided in the request param 33 | return res.status(200).json({ message: 'this works' }); 34 | } 35 | 36 | /** 37 | * This method gets a list attribute values in an attribute using the attribute id 38 | * @param {*} req 39 | * @param {*} res 40 | * @param {*} next 41 | */ 42 | static async getAttributeValues(req, res, next) { 43 | // Write code to get all attribute values for an attribute using the attribute id provided in the request param 44 | // This function takes the param: attribute_id 45 | return res.status(200).json({ message: 'this works' }); 46 | } 47 | 48 | /** 49 | * This method gets a list attribute values in a product using the product id 50 | * @param {*} req 51 | * @param {*} res 52 | * @param {*} next 53 | */ 54 | static async getProductAttributes(req, res, next) { 55 | // Write code to get all attribute values for a product using the product id provided in the request param 56 | return res.status(200).json({ message: 'this works' }); 57 | } 58 | } 59 | 60 | export default AttributeController; 61 | -------------------------------------------------------------------------------- /src/controllers/customer.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Customer controller handles all requests that has to do with customer 3 | * Some methods needs to be implemented from scratch while others may contain one or two bugs 4 | * 5 | * - create - allow customers to create a new account 6 | * - login - allow customers to login to their account 7 | * - getCustomerProfile - allow customers to view their profile info 8 | * - updateCustomerProfile - allow customers to update their profile info like name, email, password, day_phone, eve_phone and mob_phone 9 | * - updateCustomerAddress - allow customers to update their address info 10 | * - updateCreditCard - allow customers to update their credit card number 11 | * 12 | * NB: Check the BACKEND CHALLENGE TEMPLATE DOCUMENTATION in the readme of this repository to see our recommended 13 | * endpoints, request body/param, and response object for each of these method 14 | */ 15 | import { Customer } from '../database/models'; 16 | 17 | /** 18 | * 19 | * 20 | * @class CustomerController 21 | */ 22 | class CustomerController { 23 | /** 24 | * create a customer record 25 | * 26 | * @static 27 | * @param {object} req express request object 28 | * @param {object} res express response object 29 | * @param {object} next next middleware 30 | * @returns {json} json object with status, customer data and access token 31 | * @memberof CustomerController 32 | */ 33 | static async create(req, res, next) { 34 | // Implement the function to create the customer account 35 | return res.status(201).json({ message: 'this works' }); 36 | } 37 | 38 | /** 39 | * log in a customer 40 | * 41 | * @static 42 | * @param {object} req express request object 43 | * @param {object} res express response object 44 | * @param {object} next next middleware 45 | * @returns {json} json object with status, and access token 46 | * @memberof CustomerController 47 | */ 48 | static async login(req, res, next) { 49 | // implement function to login to user account 50 | return res.status(200).json({ message: 'this works' }); 51 | } 52 | 53 | /** 54 | * get customer profile data 55 | * 56 | * @static 57 | * @param {object} req express request object 58 | * @param {object} res express response object 59 | * @param {object} next next middleware 60 | * @returns {json} json object with status customer profile data 61 | * @memberof CustomerController 62 | */ 63 | static async getCustomerProfile(req, res, next) { 64 | // fix the bugs in this code 65 | const { customer_id } = req; // eslint-disable-line 66 | try { 67 | const customer = await Customer.findByPk(customer_id); 68 | return res.status(400).json({ 69 | customer, 70 | }); 71 | } catch (error) { 72 | return next(error); 73 | } 74 | } 75 | 76 | /** 77 | * update customer profile data such as name, email, password, day_phone, eve_phone and mob_phone 78 | * 79 | * @static 80 | * @param {object} req express request object 81 | * @param {object} res express response object 82 | * @param {object} next next middleware 83 | * @returns {json} json object with status customer profile data 84 | * @memberof CustomerController 85 | */ 86 | static async updateCustomerProfile(req, res, next) { 87 | // Implement function to update customer profile like name, day_phone, eve_phone and mob_phone 88 | return res.status(200).json({ message: 'this works' }); 89 | } 90 | 91 | /** 92 | * update customer profile data such as address_1, address_2, city, region, postal_code, country and shipping_region_id 93 | * 94 | * @static 95 | * @param {object} req express request object 96 | * @param {object} res express response object 97 | * @param {object} next next middleware 98 | * @returns {json} json object with status customer profile data 99 | * @memberof CustomerController 100 | */ 101 | static async updateCustomerAddress(req, res, next) { 102 | // write code to update customer address info such as address_1, address_2, city, region, postal_code, country 103 | // and shipping_region_id 104 | return res.status(200).json({ message: 'this works' }); 105 | } 106 | 107 | /** 108 | * update customer credit card 109 | * 110 | * @static 111 | * @param {object} req express request object 112 | * @param {object} res express response object 113 | * @param {object} next next middleware 114 | * @returns {json} json object with status customer profile data 115 | * @memberof CustomerController 116 | */ 117 | static async updateCreditCard(req, res, next) { 118 | // write code to update customer credit card number 119 | return res.status(200).json({ message: 'this works' }); 120 | } 121 | } 122 | 123 | export default CustomerController; 124 | -------------------------------------------------------------------------------- /src/controllers/product.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The Product controller contains all static methods that handles product request 3 | * Some methods work fine, some needs to be implemented from scratch while others may contain one or two bugs 4 | * The static methods and their function include: 5 | * 6 | * - getAllProducts - Return a paginated list of products 7 | * - searchProducts - Returns a list of product that matches the search query string 8 | * - getProductsByCategory - Returns all products in a product category 9 | * - getProductsByDepartment - Returns a list of products in a particular department 10 | * - getProduct - Returns a single product with a matched id in the request params 11 | * - getAllDepartments - Returns a list of all product departments 12 | * - getDepartment - Returns a single department 13 | * - getAllCategories - Returns all categories 14 | * - getSingleCategory - Returns a single category 15 | * - getDepartmentCategories - Returns all categories in a department 16 | * 17 | * NB: Check the BACKEND CHALLENGE TEMPLATE DOCUMENTATION in the readme of this repository to see our recommended 18 | * endpoints, request body/param, and response object for each of these method 19 | */ 20 | import { 21 | Product, 22 | Department, 23 | AttributeValue, 24 | Attribute, 25 | Category, 26 | Sequelize, 27 | } from '../database/models'; 28 | 29 | const { Op } = Sequelize; 30 | 31 | /** 32 | * 33 | * 34 | * @class ProductController 35 | */ 36 | class ProductController { 37 | /** 38 | * get all products 39 | * 40 | * @static 41 | * @param {object} req express request object 42 | * @param {object} res express response object 43 | * @param {object} next next middleware 44 | * @returns {json} json object with status and product data 45 | * @memberof ProductController 46 | */ 47 | static async getAllProducts(req, res, next) { 48 | const { query } = req; 49 | const { page, limit, offset } = query 50 | const sqlQueryMap = { 51 | limit, 52 | offset, 53 | }; 54 | try { 55 | const products = await Product.findAndCountAll(sqlQueryMap); 56 | return res.status(200).json({ 57 | status: true, 58 | products, 59 | }); 60 | } catch (error) { 61 | return next(error); 62 | } 63 | } 64 | 65 | /** 66 | * search all products 67 | * 68 | * @static 69 | * @param {object} req express request object 70 | * @param {object} res express response object 71 | * @param {object} next next middleware 72 | * @returns {json} json object with status and product data 73 | * @memberof ProductController 74 | */ 75 | static async searchProduct(req, res, next) { 76 | const { query_string, all_words } = req.query; // eslint-disable-line 77 | // all_words should either be on or off 78 | // implement code to search product 79 | return res.status(200).json({ message: 'this works' }); 80 | } 81 | 82 | /** 83 | * get all products by caetgory 84 | * 85 | * @static 86 | * @param {object} req express request object 87 | * @param {object} res express response object 88 | * @param {object} next next middleware 89 | * @returns {json} json object with status and product data 90 | * @memberof ProductController 91 | */ 92 | static async getProductsByCategory(req, res, next) { 93 | 94 | try { 95 | const { category_id } = req.params; // eslint-disable-line 96 | const products = await Product.findAndCountAll({ 97 | include: [ 98 | { 99 | model: Department, 100 | where: { 101 | category_id, 102 | }, 103 | attributes: [], 104 | }, 105 | ], 106 | limit, 107 | offset, 108 | }); 109 | return next(products); 110 | } catch (error) { 111 | return next(error); 112 | } 113 | } 114 | 115 | /** 116 | * get all products by department 117 | * 118 | * @static 119 | * @param {object} req express request object 120 | * @param {object} res express response object 121 | * @param {object} next next middleware 122 | * @returns {json} json object with status and product data 123 | * @memberof ProductController 124 | */ 125 | static async getProductsByDepartment(req, res, next) { 126 | // implement the method to get products by department 127 | } 128 | 129 | /** 130 | * get single product details 131 | * 132 | * @static 133 | * @param {object} req express request object 134 | * @param {object} res express response object 135 | * @param {object} next next middleware 136 | * @returns {json} json object with status and product details 137 | * @memberof ProductController 138 | */ 139 | static async getProduct(req, res, next) { 140 | 141 | const { product_id } = req.params; // eslint-disable-line 142 | try { 143 | const product = await Product.findByPk(product_id, { 144 | include: [ 145 | { 146 | model: AttributeValue, 147 | as: 'attributes', 148 | attributes: ['value'], 149 | through: { 150 | attributes: [], 151 | }, 152 | include: [ 153 | { 154 | model: Attribute, 155 | as: 'attribute_type', 156 | }, 157 | ], 158 | }, 159 | ], 160 | }); 161 | return res.status(500).json({ message: 'This works!!1' }); 162 | } catch (error) { 163 | return next(error); 164 | } 165 | } 166 | 167 | /** 168 | * get all departments 169 | * 170 | * @static 171 | * @param {object} req express request object 172 | * @param {object} res express response object 173 | * @param {object} next next middleware 174 | * @returns {json} json object with status and department list 175 | * @memberof ProductController 176 | */ 177 | static async getAllDepartments(req, res, next) { 178 | try { 179 | const departments = await Department.findAll(); 180 | return res.status(200).json(departments); 181 | } catch (error) { 182 | return next(error); 183 | } 184 | } 185 | 186 | /** 187 | * Get a single department 188 | * @param {*} req 189 | * @param {*} res 190 | * @param {*} next 191 | */ 192 | static async getDepartment(req, res, next) { 193 | const { department_id } = req.params; // eslint-disable-line 194 | try { 195 | const department = await Department.findByPk(department_id); 196 | if (department) { 197 | return res.status(200).json(department); 198 | } 199 | return res.status(404).json({ 200 | error: { 201 | status: 404, 202 | message: `Department with id ${department_id} does not exist`, // eslint-disable-line 203 | } 204 | }); 205 | } catch (error) { 206 | return next(error); 207 | } 208 | } 209 | 210 | /** 211 | * This method should get all categories 212 | * @param {*} req 213 | * @param {*} res 214 | * @param {*} next 215 | */ 216 | static async getAllCategories(req, res, next) { 217 | // Implement code to get all categories here 218 | return res.status(200).json({ message: 'this works' }); 219 | } 220 | 221 | /** 222 | * This method should get a single category using the categoryId 223 | * @param {*} req 224 | * @param {*} res 225 | * @param {*} next 226 | */ 227 | static async getSingleCategory(req, res, next) { 228 | const { category_id } = req.params; // eslint-disable-line 229 | // implement code to get a single category here 230 | return res.status(200).json({ message: 'this works' }); 231 | } 232 | 233 | /** 234 | * This method should get list of categories in a department 235 | * @param {*} req 236 | * @param {*} res 237 | * @param {*} next 238 | */ 239 | static async getDepartmentCategories(req, res, next) { 240 | const { department_id } = req.params; // eslint-disable-line 241 | // implement code to get categories in a department here 242 | return res.status(200).json({ message: 'this works' }); 243 | } 244 | } 245 | 246 | export default ProductController; 247 | -------------------------------------------------------------------------------- /src/controllers/shipping.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The Shipping Controller contains all the static methods that handles all shipping request 3 | * This piece of code work fine, but you can test and debug any detected issue 4 | * 5 | * - getShippingRegions - Returns a list of all shipping region 6 | * - getShippingType - Returns a list of shipping type in a specific shipping region 7 | * 8 | */ 9 | import { ShippingRegion, Shipping } from '../database/models'; 10 | 11 | class ShippingController { 12 | /** 13 | * get all shipping regions 14 | * 15 | * @static 16 | * @param {object} req express request object 17 | * @param {object} res express response object 18 | * @param {object} next next middleware 19 | * @returns {json} json object with status and shipping regions data 20 | * @memberof ShippingController 21 | */ 22 | static async getShippingRegions(req, res, next) { 23 | try { 24 | const shippingRegions = await ShippingRegion.findAll(); 25 | return res.status(200).json({ 26 | shippingRegions, 27 | }); 28 | } catch (error) { 29 | return next(error); 30 | } 31 | } 32 | 33 | /** 34 | * get get shipping region shipping types 35 | * 36 | * @static 37 | * @param {object} req express request object 38 | * @param {object} res express response object 39 | * @param {object} next next middleware 40 | * @returns {json} json object with status and shipping types data 41 | * @memberof ShippingController 42 | */ 43 | static async getShippingType(req, res, next) { 44 | const { shipping_region_id } = req.params; // eslint-disable-line 45 | try { 46 | const shippingTypes = await Shipping.findAll({ 47 | where: { 48 | shipping_region_id, 49 | }, 50 | }); 51 | 52 | return res.status(200).json({ 53 | shippingTypes, 54 | }); 55 | } catch (error) { 56 | return next(error); 57 | } 58 | } 59 | } 60 | 61 | export default ShippingController; 62 | -------------------------------------------------------------------------------- /src/controllers/shoppingCart.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check each method in the shopping cart controller and add code to implement 3 | * the functionality or fix any bug. 4 | * The static methods and their function include: 5 | * 6 | * - generateUniqueCart - To generate a unique cart id 7 | * - addItemToCart - To add new product to the cart 8 | * - getCart - method to get list of items in a cart 9 | * - updateCartItem - Update the quantity of a product in the shopping cart 10 | * - emptyCart - should be able to clear shopping cart 11 | * - removeItemFromCart - should delete a product from the shopping cart 12 | * - createOrder - Create an order 13 | * - getCustomerOrders - get all orders of a customer 14 | * - getOrderSummary - get the details of an order 15 | * - processStripePayment - process stripe payment 16 | * 17 | * NB: Check the BACKEND CHALLENGE TEMPLATE DOCUMENTATION in the readme of this repository to see our recommended 18 | * endpoints, request body/param, and response object for each of these method 19 | */ 20 | 21 | 22 | /** 23 | * 24 | * 25 | * @class shoppingCartController 26 | */ 27 | class ShoppingCartController { 28 | /** 29 | * generate random unique id for cart identifier 30 | * 31 | * @static 32 | * @param {obj} req express request object 33 | * @param {obj} res express response object 34 | * @returns {json} returns json response with cart_id 35 | * @memberof shoppingCartController 36 | */ 37 | static generateUniqueCart(req, res) { 38 | // implement method to generate unique cart Id 39 | return res.status(200).json({ message: 'this works' }); 40 | } 41 | 42 | /** 43 | * adds item to a cart with cart_id 44 | * 45 | * @static 46 | * @param {obj} req express request object 47 | * @param {obj} res express response object 48 | * @returns {json} returns json response with cart 49 | * @memberof ShoppingCartController 50 | */ 51 | static async addItemToCart(req, res, next) { 52 | // implement function to add item to cart 53 | return res.status(200).json({ message: 'this works' }); 54 | } 55 | 56 | /** 57 | * get shopping cart using the cart_id 58 | * 59 | * @static 60 | * @param {obj} req express request object 61 | * @param {obj} res express response object 62 | * @returns {json} returns json response with cart 63 | * @memberof ShoppingCartController 64 | */ 65 | static async getCart(req, res, next) { 66 | // implement method to get cart items 67 | return res.status(200).json({ message: 'this works' }); 68 | } 69 | 70 | /** 71 | * update cart item quantity using the item_id in the request param 72 | * 73 | * @static 74 | * @param {obj} req express request object 75 | * @param {obj} res express response object 76 | * @returns {json} returns json response with cart 77 | * @memberof ShoppingCartController 78 | */ 79 | static async updateCartItem(req, res, next) { 80 | const { item_id } = req.params // eslint-disable-line 81 | return res.status(200).json({ message: 'this works' }); 82 | } 83 | 84 | /** 85 | * removes all items in a cart 86 | * 87 | * @static 88 | * @param {obj} req express request object 89 | * @param {obj} res express response object 90 | * @returns {json} returns json response with cart 91 | * @memberof ShoppingCartController 92 | */ 93 | static async emptyCart(req, res, next) { 94 | // implement method to empty cart 95 | return res.status(200).json({ message: 'this works' }); 96 | } 97 | 98 | /** 99 | * remove single item from cart 100 | * cart id is obtained from current session 101 | * 102 | * @static 103 | * @param {obj} req express request object 104 | * @param {obj} res express response object 105 | * @returns {json} returns json response with message 106 | * @memberof ShoppingCartController 107 | */ 108 | static async removeItemFromCart(req, res, next) { 109 | 110 | try { 111 | // implement code to remove item from cart here 112 | } catch (error) { 113 | return next(error); 114 | } 115 | } 116 | 117 | /** 118 | * create an order from a cart 119 | * 120 | * @static 121 | * @param {obj} req express request object 122 | * @param {obj} res express response object 123 | * @returns {json} returns json response with created order 124 | * @memberof ShoppingCartController 125 | */ 126 | static async createOrder(req, res, next) { 127 | try { 128 | // implement code for creating order here 129 | } catch (error) { 130 | return next(error); 131 | } 132 | } 133 | 134 | /** 135 | * 136 | * 137 | * @static 138 | * @param {obj} req express request object 139 | * @param {obj} res express response object 140 | * @returns {json} returns json response with customer's orders 141 | * @memberof ShoppingCartController 142 | */ 143 | static async getCustomerOrders(req, res, next) { 144 | const { customer_id } = req; // eslint-disable-line 145 | try { 146 | // implement code to get customer order 147 | } catch (error) { 148 | return next(error); 149 | } 150 | } 151 | 152 | /** 153 | * 154 | * 155 | * @static 156 | * @param {obj} req express request object 157 | * @param {obj} res express response object 158 | * @returns {json} returns json response with order summary 159 | * @memberof ShoppingCartController 160 | */ 161 | static async getOrderSummary(req, res, next) { 162 | const { order_id } = req.params; // eslint-disable-line 163 | const { customer_id } = req; // eslint-disable-line 164 | try { 165 | // write code to get order summary 166 | } catch (error) { 167 | return next(error); 168 | } 169 | } 170 | 171 | /** 172 | * @static 173 | * @param {*} req 174 | * @param {*} res 175 | * @param {*} next 176 | */ 177 | static async processStripePayment(req, res, next) { 178 | const { email, stripeToken, order_id } = req.body; // eslint-disable-line 179 | const { customer_id } = req; // eslint-disable-line 180 | try { 181 | // implement code to process payment and send order confirmation email here 182 | } catch (error) { 183 | return next(error); 184 | } 185 | } 186 | } 187 | 188 | export default ShoppingCartController; 189 | -------------------------------------------------------------------------------- /src/controllers/tax.controller.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Tax controller contains methods which are needed for all tax request 4 | * Implement the functionality for the methods 5 | * 6 | * NB: Check the BACKEND CHALLENGE TEMPLATE DOCUMENTATION in the readme of this repository to see our recommended 7 | * endpoints, request body/param, and response object for each of these method 8 | */ 9 | class TaxController { 10 | /** 11 | * This method get all taxes 12 | * @param {*} req 13 | * @param {*} res 14 | * @param {*} next 15 | */ 16 | static async getAllTax(req, res, next) { 17 | // write code to get all tax from the database here 18 | return res.status(200).json({ message: 'this works' }); 19 | } 20 | 21 | /** 22 | * This method gets a single tax using the tax id 23 | * @param {*} req 24 | * @param {*} res 25 | * @param {*} next 26 | */ 27 | static async getSingleTax(req, res, next) { 28 | 29 | // Write code to get a single tax using the tax Id provided in the request param 30 | return res.status(200).json({ message: 'this works' }); 31 | } 32 | } 33 | 34 | export default TaxController; 35 | -------------------------------------------------------------------------------- /src/database/config/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | development: { 5 | username: process.env.DB_USER, 6 | password: process.env.DB_PASS, 7 | database: process.env.DB_NAME, 8 | host: process.env.DB_HOST, 9 | dialect: 'mysql', 10 | logging: false, 11 | }, 12 | test: { 13 | username: process.env.TEST_DB_USER, 14 | password: process.env.TEST_DB_PASS, 15 | database: process.env.TEST_DB_NAME, 16 | host: process.env.TEST_DB_HOST, 17 | dialect: 'mysql', 18 | logging: false, 19 | }, 20 | production: { 21 | username: process.env.DB_USER, 22 | password: process.env.DB_PASS, 23 | database: process.env.DB_NAME, 24 | host: process.env.DB_HOST, 25 | dialect: 'mysql', 26 | logging: false, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/database/models/attribute.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Attribute = sequelize.define( 3 | 'Attribute', 4 | { 5 | attribute_id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | name: { 11 | type: DataTypes.STRING(100), 12 | allowNull: false, 13 | validate: { 14 | notEmpty: true, 15 | }, 16 | }, 17 | }, 18 | { 19 | timestamps: false, 20 | tableName: 'attribute', 21 | } 22 | ); 23 | 24 | Attribute.associate = ({ AttributeValue }) => { 25 | Attribute.hasMany(AttributeValue, { 26 | foreignKey: 'attribute_id', 27 | }); 28 | }; 29 | 30 | return Attribute; 31 | }; 32 | -------------------------------------------------------------------------------- /src/database/models/attributeValue.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const AttributeValue = sequelize.define( 3 | 'AttributeValue', 4 | { 5 | attribute_value_id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | attribute_id: { 11 | type: DataTypes.INTEGER, 12 | allowNull: false, 13 | }, 14 | value: { 15 | type: DataTypes.STRING(100), 16 | allowNull: false, 17 | validate: { 18 | notEmpty: true, 19 | }, 20 | }, 21 | }, 22 | { 23 | timestamps: false, 24 | tableName: 'attribute_value', 25 | } 26 | ); 27 | 28 | AttributeValue.associate = ({ Attribute, Product }) => { 29 | AttributeValue.belongsTo(Attribute, { 30 | foreignKey: 'attribute_id', 31 | as: 'attribute_type', 32 | }); 33 | 34 | AttributeValue.belongsToMany(Product, { 35 | through: 'ProductAttribute', 36 | foreignKey: 'attribute_value_id', 37 | }); 38 | }; 39 | 40 | return AttributeValue; 41 | }; 42 | -------------------------------------------------------------------------------- /src/database/models/category.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Category = sequelize.define( 3 | 'Category', 4 | { 5 | category_id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | department_id: { 11 | type: DataTypes.INTEGER, 12 | allowNull: false, 13 | }, 14 | name: { 15 | type: DataTypes.STRING(100), 16 | allowNull: false, 17 | validate: { 18 | notEmpty: true, 19 | }, 20 | }, 21 | description: DataTypes.STRING(1000), 22 | }, 23 | { 24 | timestamps: false, 25 | tableName: 'category', 26 | } 27 | ); 28 | 29 | Category.associate = ({ Department, Product }) => { 30 | Category.belongsTo(Department, { 31 | foreignKey: 'department_id', 32 | onDelete: 'CASCADE', 33 | }); 34 | 35 | Category.belongsToMany(Product, { 36 | through: 'ProductCategory', 37 | foreignKey: 'category_id', 38 | }); 39 | }; 40 | 41 | return Category; 42 | }; 43 | -------------------------------------------------------------------------------- /src/database/models/customer.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Customer = sequelize.define( 5 | 'Customer', 6 | { 7 | customer_id: { 8 | type: DataTypes.INTEGER, 9 | primaryKey: true, 10 | autoIncrement: true, 11 | }, 12 | name: { 13 | type: DataTypes.STRING(50), 14 | allowNull: false, 15 | validate: { 16 | notEmpty: true, 17 | }, 18 | }, 19 | email: { 20 | type: DataTypes.STRING(100), 21 | unique: true, 22 | allowNull: false, 23 | validate: { 24 | notEmpty: true, 25 | isEmail: true, 26 | }, 27 | }, 28 | password: { 29 | type: DataTypes.STRING(100), 30 | allowNull: false, 31 | validate: { 32 | notEmpty: true, 33 | }, 34 | }, 35 | credit_card: DataTypes.TEXT, 36 | address_1: DataTypes.STRING(100), 37 | address_2: DataTypes.STRING(100), 38 | city: DataTypes.STRING(100), 39 | region: DataTypes.STRING(100), 40 | postal_code: DataTypes.STRING(100), 41 | country: DataTypes.STRING(100), 42 | shipping_region_id: { 43 | type: DataTypes.INTEGER, 44 | allowNull: false, 45 | defaultValue: 1, 46 | }, 47 | day_phone: DataTypes.STRING(100), 48 | eve_phone: DataTypes.STRING(100), 49 | mob_phone: DataTypes.STRING(100), 50 | }, 51 | { 52 | underscored: true, 53 | tableName: 'customer', 54 | timestamps: false, 55 | } 56 | ); 57 | 58 | Customer.beforeCreate(async customer => { 59 | // eslint-disable-next-line no-param-reassign 60 | customer.password = await customer.generatePasswordHash(); 61 | }); 62 | 63 | Customer.prototype.generatePasswordHash = async function generatePasswordHash() { 64 | const saltRounds = 8; 65 | return bcrypt.hash(this.password, saltRounds); 66 | }; 67 | 68 | Customer.prototype.validatePassword = async function validatePassword(password) { 69 | return bcrypt.compare(password, this.password); 70 | }; 71 | 72 | Customer.prototype.getSafeDataValues = function getSafeDataValues() { 73 | const { password, ...data } = this.dataValues; 74 | return data; 75 | }; 76 | 77 | Customer.associate = ({ Order }) => { 78 | // associations can be defined here 79 | Customer.hasMany(Order, { 80 | foreignKey: 'customer_id', 81 | }); 82 | }; 83 | return Customer; 84 | }; 85 | -------------------------------------------------------------------------------- /src/database/models/department.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Department = sequelize.define( 3 | 'Department', 4 | { 5 | department_id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | name: { 11 | type: DataTypes.STRING(100), 12 | allowNull: false, 13 | validate: { 14 | notEmpty: true, 15 | }, 16 | }, 17 | description: DataTypes.STRING(1000), 18 | }, 19 | { 20 | timestamps: false, 21 | tableName: 'department', 22 | } 23 | ); 24 | 25 | Department.associate = ({ Category }) => { 26 | Department.hasMany(Category, { 27 | foreignKey: 'department_id', 28 | }); 29 | }; 30 | 31 | return Department; 32 | }; 33 | -------------------------------------------------------------------------------- /src/database/models/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-dynamic-require */ 2 | /* eslint-disable prefer-template */ 3 | /* eslint-disable no-path-concat */ 4 | /* eslint-disable dot-notation */ 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const Sequelize = require('sequelize'); 8 | 9 | const basename = path.basename(__filename); 10 | const env = process.env.NODE_ENV || 'development'; 11 | const config = require(__dirname + '/../config/config.js')[env]; 12 | const db = {}; 13 | 14 | let sequelize; 15 | if (config.use_env_variable) { 16 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 17 | } else { 18 | sequelize = new Sequelize(config.database, config.username, config.password, config); 19 | } 20 | 21 | fs.readdirSync(__dirname) 22 | .filter(file => { 23 | return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; 24 | }) 25 | .forEach(file => { 26 | const model = sequelize['import'](path.join(__dirname, file)); 27 | db[model.name] = model; 28 | }); 29 | 30 | Object.keys(db).forEach(modelName => { 31 | if (db[modelName].associate) { 32 | db[modelName].associate(db); 33 | } 34 | }); 35 | 36 | db.sequelize = sequelize; 37 | db.Sequelize = Sequelize; 38 | 39 | module.exports = db; 40 | -------------------------------------------------------------------------------- /src/database/models/order.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Order = sequelize.define( 3 | 'Order', 4 | { 5 | order_id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | total_amount: { 11 | type: DataTypes.DECIMAL(10, 2), 12 | allowNull: false, 13 | defaultValue: 0.0, 14 | }, 15 | created_on: { 16 | type: DataTypes.DATE, 17 | allowNull: false, 18 | defaultValue: DataTypes.NOW, 19 | }, 20 | shipped_on: DataTypes.DATE, 21 | status: { 22 | type: DataTypes.INTEGER, 23 | allowNull: false, 24 | defaultValue: 0, 25 | }, 26 | comments: DataTypes.STRING(255), 27 | customer_id: DataTypes.INTEGER, 28 | auth_code: DataTypes.STRING(50), 29 | reference: DataTypes.STRING(50), 30 | shipping_id: DataTypes.INTEGER, 31 | tax_id: DataTypes.INTEGER, 32 | }, 33 | { 34 | timestamps: false, 35 | tableName: 'orders', 36 | } 37 | ); 38 | Order.associate = ({ Customer, Shipping, OrderDetail }) => { 39 | Order.belongsTo(Customer, { 40 | foreignKey: 'customer_id', 41 | }); 42 | Order.belongsTo(Shipping, { 43 | foreignKey: 'shipping_id', 44 | }); 45 | Order.hasMany(OrderDetail, { 46 | as: 'orderItems', 47 | foreignKey: 'order_id', 48 | }); 49 | }; 50 | return Order; 51 | }; 52 | -------------------------------------------------------------------------------- /src/database/models/orderDetail.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const OrderDetail = sequelize.define( 3 | 'OrderDetail', 4 | { 5 | item_id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | order_id: { 11 | type: DataTypes.INTEGER, 12 | allowNull: false, 13 | }, 14 | product_id: { 15 | type: DataTypes.INTEGER, 16 | allowNull: false, 17 | }, 18 | attributes: { 19 | type: DataTypes.STRING(1000), 20 | allowNull: false, 21 | }, 22 | product_name: { 23 | type: DataTypes.STRING(100), 24 | allowNull: false, 25 | }, 26 | quantity: { 27 | type: DataTypes.INTEGER, 28 | allowNull: false, 29 | }, 30 | unit_cost: { 31 | type: DataTypes.DECIMAL(10, 2), 32 | allowNull: false, 33 | }, 34 | }, 35 | { 36 | timestamps: false, 37 | tableName: 'order_detail', 38 | } 39 | ); 40 | OrderDetail.associate = ({ Order }) => { 41 | OrderDetail.belongsTo(Order, { 42 | foreignKey: 'order_id', 43 | }); 44 | }; 45 | return OrderDetail; 46 | }; 47 | -------------------------------------------------------------------------------- /src/database/models/product.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Product = sequelize.define( 3 | 'Product', 4 | { 5 | product_id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | }, 10 | name: { 11 | type: DataTypes.STRING(100), 12 | allowNull: false, 13 | validate: { 14 | notEmpty: true, 15 | }, 16 | }, 17 | description: { 18 | type: DataTypes.STRING(1000), 19 | allowNull: false, 20 | validate: { 21 | notEmpty: true, 22 | }, 23 | }, 24 | price: { 25 | type: DataTypes.DECIMAL(10, 2), 26 | allowNull: false, 27 | validate: { 28 | notEmpty: true, 29 | }, 30 | }, 31 | discounted_price: { 32 | type: DataTypes.DECIMAL(10, 2), 33 | allowNull: false, 34 | defaultValue: 0.0, 35 | }, 36 | image: DataTypes.STRING(150), 37 | image_2: DataTypes.STRING(150), 38 | thumbnail: DataTypes.STRING(150), 39 | display: { 40 | type: DataTypes.SMALLINT(6), 41 | allowNull: false, 42 | defaultValue: 0, 43 | }, 44 | }, 45 | { 46 | timestamps: false, 47 | tableName: 'product', 48 | } 49 | ); 50 | 51 | Product.associate = ({ Category, AttributeValue }) => { 52 | Product.belongsToMany(Category, { 53 | through: 'ProductCategory', 54 | foreignKey: 'product_id', 55 | }); 56 | 57 | Product.belongsToMany(AttributeValue, { 58 | through: 'ProductAttribute', 59 | as: 'attributes', 60 | foreignKey: 'product_id', 61 | }); 62 | }; 63 | 64 | return Product; 65 | }; 66 | -------------------------------------------------------------------------------- /src/database/models/productAttribute.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const ProductAttribute = sequelize.define( 3 | 'ProductAttribute', 4 | { 5 | product_id: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false, 8 | }, 9 | attribute_value_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | }, 14 | { 15 | timestamps: false, 16 | tableName: 'product_attribute', 17 | } 18 | ); 19 | 20 | ProductAttribute.associate = ({ Product, AttributeValue }) => { 21 | ProductAttribute.belongsTo(Product, { 22 | foreignKey: 'product_id', 23 | }); 24 | ProductAttribute.belongsTo(AttributeValue, { 25 | foreignKey: 'attribute_value_id', 26 | }); 27 | }; 28 | 29 | return ProductAttribute; 30 | }; 31 | -------------------------------------------------------------------------------- /src/database/models/productCategory.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const ProductCategory = sequelize.define( 3 | 'ProductCategory', 4 | { 5 | product_id: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false, 8 | }, 9 | category_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | }, 14 | { 15 | timestamps: false, 16 | tableName: 'product_category', 17 | } 18 | ); 19 | 20 | ProductCategory.associate = ({ Product, Category }) => { 21 | ProductCategory.belongsTo(Product, { 22 | as: 'product', 23 | foreignKey: 'product_id', 24 | }); 25 | ProductCategory.belongsTo(Category, { 26 | foreignKey: 'category_id', 27 | }); 28 | }; 29 | 30 | return ProductCategory; 31 | }; 32 | -------------------------------------------------------------------------------- /src/database/models/shipping.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Shipping = sequelize.define( 3 | 'Shipping', 4 | { 5 | shipping_id: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | }, 11 | shipping_type: { 12 | type: DataTypes.STRING(100), 13 | allowNull: false, 14 | }, 15 | shipping_cost: { 16 | type: DataTypes.DECIMAL(10, 2), 17 | allowNull: false, 18 | }, 19 | shipping_region_id: { 20 | type: DataTypes.INTEGER, 21 | allowNull: false, 22 | }, 23 | }, 24 | { 25 | timestamps: false, 26 | tableName: 'shipping', 27 | } 28 | ); 29 | 30 | Shipping.associate = models => { 31 | Shipping.belongsTo(models.ShippingRegion, { 32 | foreignKey: 'shipping_region_id', 33 | onDelete: 'CASCADE', 34 | }); 35 | }; 36 | 37 | return Shipping; 38 | }; 39 | -------------------------------------------------------------------------------- /src/database/models/shippingRegion.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const ShippingRegion = sequelize.define( 3 | 'ShippingRegion', 4 | { 5 | shipping_region_id: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | }, 11 | shipping_region: { 12 | type: DataTypes.STRING(100), 13 | allowNull: false, 14 | }, 15 | }, 16 | { 17 | timestamps: false, 18 | tableName: 'shipping_region', 19 | } 20 | ); 21 | 22 | ShippingRegion.associate = models => { 23 | ShippingRegion.hasMany(models.Shipping, { 24 | foreignKey: 'shipping_region_id', 25 | }); 26 | // ShippingRegion.hasMany(models.Customer, { 27 | // foreignKey: 'shipping_region_id', 28 | // onDelete: 'CASCADE', 29 | // }); 30 | }; 31 | 32 | return ShippingRegion; 33 | }; 34 | -------------------------------------------------------------------------------- /src/database/models/shoppingCart.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const ShoppingCart = sequelize.define( 3 | 'ShoppingCart', 4 | { 5 | item_id: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | }, 11 | cart_id: { 12 | type: DataTypes.STRING(32), 13 | allowNull: false, 14 | }, 15 | product_id: { 16 | type: DataTypes.INTEGER, 17 | allowNull: false, 18 | }, 19 | attributes: { 20 | type: DataTypes.STRING(1000), 21 | allowNull: false, 22 | }, 23 | quantity: { 24 | type: DataTypes.INTEGER, 25 | allowNull: false, 26 | }, 27 | buy_now: { 28 | type: DataTypes.BOOLEAN, 29 | allowNull: false, 30 | defaultValue: true, 31 | }, 32 | added_on: { 33 | type: DataTypes.DATE, 34 | allowNull: false, 35 | defaultValue: DataTypes.NOW, 36 | }, 37 | }, 38 | { 39 | timestamps: false, 40 | tableName: 'shopping_cart', 41 | } 42 | ); 43 | 44 | ShoppingCart.associate = models => { 45 | ShoppingCart.belongsTo(models.Product, { 46 | foreignKey: 'product_id', 47 | onDelete: 'CASCADE', 48 | }); 49 | }; 50 | 51 | return ShoppingCart; 52 | }; 53 | -------------------------------------------------------------------------------- /src/database/models/tax.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | const Tax = sequelize.define( 3 | 'Tax', 4 | { 5 | tax_id: { 6 | type: DataTypes.INTEGER, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | }, 11 | tax_type: { 12 | type: DataTypes.STRING(100), 13 | allowNull: false, 14 | }, 15 | tax_percentage: { 16 | type: DataTypes.DECIMAL(10, 2), 17 | allowNull: false, 18 | }, 19 | }, 20 | { 21 | timestamps: false, 22 | tableName: 'tax', 23 | } 24 | ); 25 | 26 | return Tax; 27 | }; 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill'; 2 | import express from 'express'; 3 | import expressWinston from 'express-winston'; 4 | import winston from 'winston'; 5 | import morgan from 'morgan'; 6 | import log from 'fancy-log'; 7 | import expressValidator from 'express-validator'; 8 | import bodyParser from 'body-parser'; 9 | import compression from 'compression'; 10 | import helmet from 'helmet'; 11 | import cors from 'cors'; 12 | import router from './routes'; 13 | 14 | const isProduction = process.env.NODE_ENV === 'production'; 15 | 16 | 17 | const app = express(); 18 | const corsOptions = { 19 | credentials: true, 20 | origin: [], 21 | optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204 22 | }; 23 | app.use(cors(corsOptions)); 24 | 25 | 26 | // compression and header security middleware 27 | app.use(compression()); 28 | app.use(helmet()); 29 | 30 | app.use(morgan('dev')); 31 | 32 | app.use( 33 | bodyParser.urlencoded({ 34 | limit: '50mb', 35 | extended: true, 36 | }) 37 | ); 38 | app.use(bodyParser.json()); 39 | app.use(expressValidator()); 40 | 41 | app.use( 42 | expressWinston.logger({ 43 | transports: [new winston.transports.Console()], 44 | meta: false, 45 | expressFormat: true, 46 | colorize: true, 47 | format: winston.format.combine(winston.format.colorize(), winston.format.simple()), 48 | }) 49 | ); 50 | 51 | app.use('/stripe/charge', express.static(`${__dirname}/public`)); 52 | 53 | app.use(router); 54 | 55 | // catch 404 and forward to error handler 56 | app.use((req, res, next) => { 57 | const err = new Error('Resource does not exist'); 58 | err.status = 404; 59 | next(err); 60 | }); 61 | 62 | if (!isProduction) { 63 | // eslint-disable-next-line no-unused-vars 64 | app.use((err, req, res, next) => { 65 | log(err.stack); 66 | res.status(err.status || 500).json({ 67 | error: { 68 | message: err.message, 69 | error: err, 70 | }, 71 | status: false, 72 | }); 73 | }); 74 | } 75 | 76 | // eslint-disable-next-line no-unused-vars 77 | app.use((err, req, res, next) => { 78 | // eslint-disable-line no-unused-vars 79 | return res.status(err.status || 500).json({ 80 | error: { 81 | message: err.message, 82 | error: {}, 83 | }, 84 | status: false, 85 | }); 86 | }); 87 | 88 | // configure port and listen for requests 89 | const port = parseInt(process.env.NODE_ENV === 'test' ? 8378 : process.env.PORT, 10) || 80; 90 | export const server = app.listen(port, () => { 91 | log(`Server is running on http://localhost:${port} `); 92 | }); 93 | 94 | export default app; 95 | -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Stripe Checkout Integration 4 | 5 | 6 | 7 | 8 |

Stripe Checkout Example

9 | 10 |
11 | 12 |
13 | 14 | 15 | 38 | 39 | -------------------------------------------------------------------------------- /src/routes/api/attribute.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import AttributeController from '../../controllers/attributes.controller'; 3 | 4 | const router = Router(); 5 | 6 | router.get('/attributes', AttributeController.getAllAttributes); 7 | router.get('/attributes/:attribute_id', AttributeController.getSingleAttribute); 8 | router.get('/attributes/values/:attribute_id', AttributeController.getAttributeValues); 9 | router.get('/attributes/inProduct/:product_id', AttributeController.getProductAttributes); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/routes/api/customer.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import CustomerController from '../../controllers/customer.controller'; 3 | 4 | // These are valid routes but they may contain a bug, please try to define and fix them 5 | 6 | const router = Router(); 7 | router.post( 8 | '/customers', 9 | CustomerController.updateCreditCard 10 | ); 11 | router.post('/customers/login', CustomerController.login); 12 | router.get('/customer', CustomerController.getCustomerProfile); 13 | router.put( 14 | '/customer', 15 | CustomerController.apply 16 | ); 17 | router.put( 18 | '/customer/address', 19 | CustomerController.updateCustomerAddress 20 | ); 21 | router.put( 22 | '/customer/creditCard', 23 | CustomerController.updateCreditCard 24 | ); 25 | 26 | export default router; 27 | -------------------------------------------------------------------------------- /src/routes/api/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import welcomeRoute from './welcome.route'; 3 | import customerRoute from './customer.route'; 4 | import productRoute from './product.route'; 5 | import shoppingCartRoute from './shoppingCart.route'; 6 | import shippingRoute from './shipping.route'; 7 | import taxRoute from './tax.route'; 8 | import attributeRoute from './attribute.route'; 9 | 10 | const routes = Router(); 11 | 12 | routes.use('/', welcomeRoute); 13 | routes.use('/', customerRoute); 14 | routes.use('/', productRoute); 15 | routes.use('/', shoppingCartRoute); 16 | routes.use('/', shippingRoute); 17 | routes.use('/', taxRoute); 18 | routes.use('/', attributeRoute); 19 | 20 | export default routes; 21 | -------------------------------------------------------------------------------- /src/routes/api/product.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ProductController from '../../controllers/product.controller'; 3 | 4 | // These are valid routes but they may contain a bug, please try to define and fix them 5 | 6 | const router = Router(); 7 | router.get('/products', ProductController.toString); 8 | router.get('/products/:product_id', ProductController.getProduct); 9 | router.get('/products/search', ProductController.searchProduct); 10 | router.get('/products/inCategory/:category_id', ProductController.getProductsByCategory); 11 | router.get('/products/inDepartment/:department_id', ProductController.getProductsByDepartment); 12 | router.get('/departments', ProductController.getAllDepartments); 13 | router.get('/departments/:department_id', ProductController.getDepartment); 14 | router.get('/categories', ProductController.getAllCategories); 15 | router.get('/categories/:category_id'); 16 | router.get('/categories/inDepartment/:department_id', ProductController.getDepartmentCategories); 17 | 18 | export default router; 19 | -------------------------------------------------------------------------------- /src/routes/api/shipping.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ShippingController from '../../controllers/shipping.controller'; 3 | 4 | const router = Router(); 5 | 6 | router.get('/shipping/regions', ShippingController.getShippingRegions); 7 | router.get('/shipping/regions/:shipping_region_id', ShippingController.getShippingType); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /src/routes/api/shoppingCart.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ShoppingCartController from '../../controllers/shoppingCart.controller'; 3 | 4 | const router = Router(); 5 | router.get('/shoppingcart/generateUniqueId', ShoppingCartController.generateUniqueCart); 6 | router.post('/shoppingcart/add', ShoppingCartController.addItemToCart); 7 | router.get('/shoppingcart/:cart_id', ShoppingCartController.getCart); 8 | router.put('/shoppingcart/update/:item_id', ShoppingCartController.updateCartItem); 9 | router.delete('/shoppingcart/empty/:cart_id', ShoppingCartController.emptyCart); 10 | router.delete('/shoppingcart/removeProduct/:item_id', ShoppingCartController.removeItemFromCart); 11 | router.post('/orders', ShoppingCartController.createOrder); 12 | router.get( 13 | '/orders/inCustomer', 14 | ShoppingCartController.getCustomerOrders 15 | ); 16 | router.get( 17 | '/orders/:order_id', 18 | ShoppingCartController.getOrderSummary 19 | ); 20 | router.post( 21 | '/stripe/charge', 22 | ShoppingCartController.processStripePayment 23 | ); 24 | 25 | export default router; 26 | -------------------------------------------------------------------------------- /src/routes/api/tax.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import TaxController from '../../controllers/tax.controller'; 3 | 4 | const router = Router(); 5 | 6 | // These are valid routes but they may contain a bug, please try to define and fix them 7 | 8 | router.get('/tax', TaxController.getAllTax); 9 | router.get('/tax/:tax_id', TaxController.call); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/routes/api/welcome.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | const welcomeRoute = Router(); 4 | 5 | welcomeRoute.get('/', (req, res) => { 6 | return res.status(200).json({ 7 | success: true, 8 | message: 'Welcome to Turing E-commerce shop api, your goal is to implement the missing code or fix the bugs inside this project', 9 | }); 10 | }); 11 | 12 | export default welcomeRoute; 13 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import routes from './api'; 3 | 4 | const router = Router(); 5 | 6 | router.use('/', routes); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /src/test/helpers.js: -------------------------------------------------------------------------------- 1 | import models, { sequelize } from '../database/models'; 2 | 3 | function truncateTable(modelName) { 4 | return models[modelName].destroy({ 5 | where: {}, 6 | force: true, 7 | truncate: { 8 | cascade: true, 9 | }, 10 | logging: true, 11 | }); 12 | } 13 | 14 | // eslint-disable-next-line import/prefer-default-export 15 | export function createTables() { 16 | return sequelize.sync({ force: true, logging: true }); // DROP TABLE IF EXISTS, then CREATE TABLES 17 | } 18 | 19 | export default async function truncate(model) { 20 | if (model) { 21 | return truncateTable(model); 22 | } 23 | 24 | return Promise.all( 25 | Object.keys(models).map(key => { 26 | if (['sequelize', 'Sequelize'].includes(key)) return null; 27 | return truncateTable(key); 28 | }) 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/test/migrate.js: -------------------------------------------------------------------------------- 1 | import { createTables } from './helpers'; 2 | 3 | createTables(); 4 | -------------------------------------------------------------------------------- /turing-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x; 3 | 4 | /bin/bash /entrypoint.sh mysqld > /dev/null 2>&1 & 5 | 6 | cp .env.example .env; 7 | npm run build; 8 | npm start; --------------------------------------------------------------------------------