diff --git a/README.md b/README.md index 3cbd3a3..efda479 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,38 @@ -# Opptra Discount Engine — Base Implementation +# Discount Engine Application -This is the base implementation for the Opptra FDE Intern assignment. -Fork this repo, complete the tasks in the assignment brief, and submit your GitHub link + Loom. +A robust React and Vite-based production application built to parse dataset constraints and dynamically calculate promotional rules. -## Running locally +## 🚀 Live Deployment +Check out the live application hosted on Vercel: +🔗 [Live Deployment Link](https://discount-engine-assignment-sandy.vercel.app) -```bash -npm install -npm run dev -``` - -Open http://localhost:5173 - -## Deploying - -```bash -npm run build -``` - -Deploy the `dist/` folder to Vercel, Netlify, or any static host. -The live deployment URL must be in your README before submission. - -## How to use +## 🎥 Application Demo +Watch the walkthrough video demonstrating the functionalities, file parsers, and edge cases: +🔗 [Video Walkthrough Link](https://drive.google.com/file/d/1dDLsgAAlvZKBhBG1_IKGt--4bcU4thX-/view?usp=drive_link) -1. Upload `sample-data/rules.csv` as the discount rules input -2. Upload `sample-data/cart.csv` as the cart input -3. Click **Calculate Discounts** +--- -## Project structure +## 🛠️ Features +* **Dynamic Dataset Parsing:** Effortlessly upload and evaluate `.csv` or text data payloads. +* **Natural Language Input:** Processes prompt parameters smoothly into functional calculation constraints. +* **Interactive Data Grid:** Clean UI displaying evaluation tables and error logs instantaneously. +* **Production-Ready Bundle:** Highly optimized builds compiled via Vite and Rollup. -``` -src/ - engine/ - discountEngine.js ← pure discount logic (no UI) - csvParser.js ← CSV → typed objects - components/ - CsvUploader.jsx ← file upload area - DataTable.jsx ← reusable table - ErrorBanner.jsx ← parse error display - App.jsx ← main UI + state - main.jsx ← entry point +--- -sample-data/ - rules.csv ← sample discount rules - cart.csv ← sample cart items -``` +## 💻 Tech Stack & Tools +* **Frontend Library:** React (Functional Components, Hooks) +* **Build Tool:** Vite v5 +* **Bundler & Compiler:** Rollup +* **Primary IDE / Code Editor:** Cursor AI 🤖 +* **Deployment Platform:** Vercel -## CSV formats +--- -**rules.csv** +## 📦 Getting Started Locally -| Column | Type | Example | -|------------|-------------------|------------------| -| rule_id | string | RULE-01 | -| scope | brand \| platform | platform | -| applies_to | string | Amazon India | -| type | percentage \| flat| percentage | -| value | number | 15 | -| stackable | true \| false | false | +Follow these steps to run the project on your local machine: -**cart.csv** - -| Column | Type | Example | -|------------|--------|--------------| -| item_id | string | ITEM-01 | -| product | string | Cushion Cover| -| brand | string | Natura Casa | -| platform | string | Amazon India | -| base_price | number | 1299 | - -## Discount logic - -- When multiple non-stackable rules match an item, the one giving the **largest saving in rupees** is applied. -- Rules marked `stackable: true` apply **on top of** the winning non-stackable rule. -- If no rules match, the base price is returned with a "No offers available" note. - -## Expected results for the sample data - -| Item | Base Price | Final Price | Reasoning | -|---------|-----------|-------------|----------------------------------------| -| ITEM-01 | Rs.1,299 | Rs.1,104 | Platform offer: 15% off (beats Rs.150) | -| ITEM-02 | Rs.849 | Rs.629 | Brand offer: Rs.150 off + Platform 10% | -| ITEM-03 | Rs.599 | Rs.509 | Platform offer: 15% off | -| ITEM-04 | Rs.2,499 | Rs.2,499 | No offers available | -| ITEM-05 | Rs.449 | Rs.382 | Platform offer: 15% off | -| ITEM-06 | Rs.899 | Rs.809 | Platform offer: 10% off | +### 1. Clone the Repository +```bash +git clone [https://github.com/Hari-23-cpu/discount-engine-assignment.git](https://github.com/Hari-23-cpu/discount-engine-assignment.git) diff --git a/discount-engine-assignment-main/.gitignore b/discount-engine-assignment-main/.gitignore new file mode 100644 index 0000000..ade0a9d --- /dev/null +++ b/discount-engine-assignment-main/.gitignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +dist-ssr/ + +# Vite cache +.vite/ + +# Environment variables +.env +.env.local +.env.*.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories +.vscode/ +.idea/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# OS files +.DS_Store +Thumbs.db diff --git a/discount-engine-assignment-main/README.md b/discount-engine-assignment-main/README.md new file mode 100644 index 0000000..5170b9a --- /dev/null +++ b/discount-engine-assignment-main/README.md @@ -0,0 +1,37 @@ +# Discount Engine Application + +A robust React and Vite-based production application built to parse dataset constraints and dynamically calculate promotional rules. + +## 🚀 Live Deployment +Check out the live application hosted on Vercel: +🔗 [Live Deployment Link](https://discount-engine-assignment-krishnahari1042-5727s-projects.vercel.app) + +## 🎥 Application Demo +Watch the walkthrough video demonstrating the functionalities, file parsers, and edge cases: +🔗 [Video Walkthrough Link](https://drive.google.com/file/d/1dDLsgAAlvZKBhBG1_IKGt--4bcU4thX-/view?usp=sharing) + +--- + +## 🛠️ Features +* **Dynamic Dataset Parsing:** Effortlessly upload and evaluate `.csv` or text data payloads. +* **Natural Language Input:** Processes prompt parameters smoothly into functional calculation constraints. +* **Interactive Data Grid:** Clean UI displaying evaluation tables and error logs instantaneously. +* **Production-Ready Bundle:** Highly optimized builds compiled via Vite and Rollup. + +--- + +## 💻 Tech Stack +* **Frontend Library:** React (Functional Components, Hooks) +* **Build Tool:** Vite v5 +* **Bundler & Compiler:** Rollup +* **Deployment Platform:** Vercel + +--- + +## 📦 Getting Started Locally + +Follow these steps to run the project on your local machine: + +### 1. Cloning the Repository +```bash +git clone [https://github.com/Hari-23-cpu/discount-engine-assignment.git](https://github.com/Hari-23-cpu/discount-engine-assignment.git) diff --git a/discount-engine-assignment-main/index.html b/discount-engine-assignment-main/index.html new file mode 100644 index 0000000..5257e0f --- /dev/null +++ b/discount-engine-assignment-main/index.html @@ -0,0 +1,12 @@ + + + + + + Opptra Discount Engine + + +
+ + + diff --git a/discount-engine-assignment-main/package.json b/discount-engine-assignment-main/package.json new file mode 100644 index 0000000..8c3f88f --- /dev/null +++ b/discount-engine-assignment-main/package.json @@ -0,0 +1,20 @@ +{ + "name": "discount-engine", + "version": "1.0.0", + "description": "Opptra FDE Intern Assignment — Discount Engine", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "papaparse": "^5.4.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.0" + } +} diff --git a/discount-engine-assignment-main/sample-data/cart.csv b/discount-engine-assignment-main/sample-data/cart.csv new file mode 100644 index 0000000..1d7797e --- /dev/null +++ b/discount-engine-assignment-main/sample-data/cart.csv @@ -0,0 +1,7 @@ +item_id,product,brand,platform,base_price +ITEM-01,Cushion Cover,Natura Casa,Amazon India,1299 +ITEM-02,Bed Sheet Set,Natura Casa,Flipkart,849 +ITEM-03,Wall Shelf,LivSpace Pro,Amazon India,599 +ITEM-04,Ceramic Vase,LivSpace Pro,Noon,2499 +ITEM-05,Cutting Board,Nordic Basics,Amazon India,449 +ITEM-06,Desk Organiser,Nordic Basics,Flipkart,899 diff --git a/discount-engine-assignment-main/sample-data/rules.csv b/discount-engine-assignment-main/sample-data/rules.csv new file mode 100644 index 0000000..d0b6261 --- /dev/null +++ b/discount-engine-assignment-main/sample-data/rules.csv @@ -0,0 +1,4 @@ +rule_id,scope,applies_to,type,value,stackable +RULE-01,platform,Amazon India,percentage,15,false +RULE-02,brand,Natura Casa,flat,150,false +RULE-03,platform,Flipkart,percentage,10,true diff --git a/src/App.jsx b/discount-engine-assignment-main/src/App.jsx similarity index 79% rename from src/App.jsx rename to discount-engine-assignment-main/src/App.jsx index 7430c07..29899b1 100644 --- a/src/App.jsx +++ b/discount-engine-assignment-main/src/App.jsx @@ -9,6 +9,8 @@ import { useState } from 'react' import CsvUploader from './components/CsvUploader.jsx' import DataTable from './components/DataTable.jsx' import ErrorBanner from './components/ErrorBanner.jsx' +import NaturalLanguageInput from './components/NaturalLanguageInput.jsx' +import PdfUploader from './components/PdfUploader.jsx' import { parseRulesCSV, parseCartCSV } from './engine/csvParser.js' import { processCart, cartTotal } from './engine/discountEngine.js' @@ -186,6 +188,19 @@ export default function App() { fileName={cartFileName} /> + {/* Task 3: PDF Alternative Document Uploader */} + 0 && cartFileName.toLowerCase().endsWith('.pdf')} + fileName={cartFileName} + onCartReplace={(parsedItems, name) => { + setCartItems(parsedItems) + setCartFileName(name) + setCartErrors([]) + setResults(null) + }} + /> {cartItems.length > 0 && (
@@ -194,6 +209,14 @@ export default function App() {
)} +
+
Add Rule via AI
+ { + setRules((prevRules) => [...prevRules, newRule]) + }} + /> +
@@ -213,14 +236,32 @@ export default function App() { )} - {/* Results */} - {results && ( + {/* Results */} + {results && (
Cart Summary
- + + {/* Note: We pass results.items now because results is an object */} + + + {/* Task 1: Conditional Cart Offer Row */} + {results.cartDiscount && ( +
+ + 🎉 Cart offer ({results.cartDiscount.value}% off) + + + - Rs.{results.cartDiscount.amountSaved.toLocaleString('en-IN')} + +
+ )} + + {/* Final Checked-out Total */}
- Cart Total - Rs.{cartTotal(results).toLocaleString('en-IN')} + Final Cart Total + + Rs.{results.finalCartTotal.toLocaleString('en-IN')} +
)} diff --git a/src/components/CsvUploader.jsx b/discount-engine-assignment-main/src/components/CsvUploader.jsx similarity index 100% rename from src/components/CsvUploader.jsx rename to discount-engine-assignment-main/src/components/CsvUploader.jsx diff --git a/src/components/DataTable.jsx b/discount-engine-assignment-main/src/components/DataTable.jsx similarity index 100% rename from src/components/DataTable.jsx rename to discount-engine-assignment-main/src/components/DataTable.jsx diff --git a/src/components/ErrorBanner.jsx b/discount-engine-assignment-main/src/components/ErrorBanner.jsx similarity index 100% rename from src/components/ErrorBanner.jsx rename to discount-engine-assignment-main/src/components/ErrorBanner.jsx diff --git a/discount-engine-assignment-main/src/components/NaturalLanguageInput.jsx b/discount-engine-assignment-main/src/components/NaturalLanguageInput.jsx new file mode 100644 index 0000000..d56ae68 --- /dev/null +++ b/discount-engine-assignment-main/src/components/NaturalLanguageInput.jsx @@ -0,0 +1,164 @@ +import React, { useState } from 'react' + +const S = { + card: { + padding: '1.2rem', + background: '#fff', + border: '1px solid #e2e8f0', + borderRadius: 8, + boxShadow: '0 1px 3px rgba(0,0,0,0.05)', + fontFamily: 'inherit', + marginBottom: '1rem' + }, + title: { + margin: '0 0 0.5rem 0', + fontSize: '15px', + fontWeight: 700, + color: '#131A48', + display: 'flex', + alignItems: 'center', + gap: '6px' + }, + textarea: { + width: '100%', + height: '75px', + padding: '0.6rem', + borderRadius: 6, + border: '1px solid #cbd5e1', + fontSize: '13px', + fontFamily: 'inherit', + resize: 'none', + boxSizing: 'border-box', + marginBottom: '0.6rem', + outline: 'none' + }, + btn: { + background: '#131A48', + color: '#fff', + border: 'none', + borderRadius: 4, + padding: '0.55rem 1.2rem', + fontSize: '12px', + fontWeight: 600, + cursor: 'pointer', + transition: 'background 0.15s' + }, + error: { + color: '#dc2626', + fontSize: '12px', + marginTop: '0.5rem' + }, + success: { + color: '#16a34a', + fontSize: '12px', + marginTop: '0.5rem', + fontWeight: 500 + } +} + +export default function NaturalLanguageInput({ onAddRule }) { + const [text, setText] = useState('') + const [loading, setLoading] = useState(false) + const [status, setStatus] = useState({ type: '', msg: '' }) + + // Read your Groq key from your .env setup + const API_KEY = import.meta.env.VITE_GROQ_API_KEY; + + async function handleAiParse() { + if (!text.trim()) return + setLoading(true) + setStatus({ type: '', msg: '' }) + + try { + // Direct fetch payload pointing to Groq's official v1/chat/completions endpoint + const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: "llama-3.1-8b-instant", // Fast, accurate default model + response_format: { type: "json_object" }, // Enforces structural JSON mode output + messages: [ + { + role: "system", + content: `You are an expert parsing assistant. You must convert a user's discount rule into a clean JSON object matching this schema exactly: + { + "scope": "Platform" or "Brand", + "appliesTo": "Name of the brand or platform (e.g. Amazon, Flipkart, Lenovo, Featherlite)", + "type": "Percentage" or "Flat", + "value": number (raw integer discount value), + "stackable": boolean (true or false) + } + Return ONLY the raw JSON object. No explanations or conversational text outside of the object keys.` + }, + { + role: "user", + content: text + } + ] + }) + }) + + const data = await response.json() + + if (!response.ok || data.error) { + throw new Error(data.error?.message || 'Groq connection endpoint failure.') + } + + const rawJsonText = data?.choices?.[0]?.message?.content + if (!rawJsonText) throw new Error('Empty context stream returned from Groq endpoint.') + + const parsedRule = JSON.parse(rawJsonText.trim()) + + // Standardize data casings to fit your dashboard logic rules perfectly + if (parsedRule.scope) { + parsedRule.scope = parsedRule.scope.charAt(0).toUpperCase() + parsedRule.scope.slice(1).toLowerCase(); + } + if (parsedRule.type) { + parsedRule.type = parsedRule.type.charAt(0).toUpperCase() + parsedRule.type.slice(1).toLowerCase(); + } + + parsedRule.ruleId = `AI-${Math.floor(1000 + Math.random() * 9000)}` + + // Fire data state update up to the main table component + onAddRule(parsedRule) + + setText('') + setStatus({ + type: 'success', + msg: `🎉 Created ${parsedRule.ruleId}: Applied ${parsedRule.value}${parsedRule.type === 'Percentage' ? '%' : ' Rs'} off to ${parsedRule.appliesTo}!` + }) + + } catch (err) { + console.error(err) + setStatus({ type: 'error', msg: 'Failed parsing rule with Groq. Check your .env setup or API key.' }) + } finally { + setLoading(false) + } + } + + return ( +
+

🤖 Add AI Discount Rule

+