diff --git a/HLD_optimized.docx b/HLD_optimized.docx new file mode 100644 index 0000000..b9d73a2 Binary files /dev/null and b/HLD_optimized.docx differ diff --git a/HLD_optimized.pptx b/HLD_optimized.pptx new file mode 100644 index 0000000..89d2174 Binary files /dev/null and b/HLD_optimized.pptx differ diff --git a/business_overview.pptx b/business_overview.pptx new file mode 100644 index 0000000..2706ca9 Binary files /dev/null and b/business_overview.pptx differ diff --git a/src/App.test.tsx b/src/App.test.tsx index 3980ec8..c51a53a 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -8,7 +8,29 @@ beforeAll(() => { globalThis.fetch = vi.fn((url) => { let responseData: any = {}; if (url.includes('/api/auth/register') || url.includes('/api/auth/login')) { - responseData = { id: 1, username: 'test_seller', email: 'test@example.com' }; + responseData = { + access_token: 'mock_access_token', + refresh_token: 'mock_refresh_token', + user: { + id: 1, + username: 'test_seller', + email: 'test@example.com', + profile: { onboarding_step: 4, phone_verified: true, email_verified: true, is_gstin_verified: true, aadhar_s3_key: 'mock_aadhar', pan_s3_key: 'mock_pan', store_name: 'Crafty Store', logo_s3_key: 'mock_logo', street: '123 Loom Lane', status: 'pending_approval' } + } + }; + } else if (url.includes('/api/auth/verify-otp')) { + responseData = { verified: true, next_step: 'complete_profile' }; + } else if (url.includes('/api/profile/submit-gstin')) { + responseData = { verified: true, business_name: 'TEST BUSINESS', address: 'Test address', gstin_status: 'Active' }; + } else if (url.includes('/api/profile/presigned-url')) { + responseData = { presigned_url: 'https://s3.mock/upload', s3_key: 'suppliers/1/mock_key.jpg' }; + } else if (url.includes('/api/profile/')) { + responseData = { + id: 1, + username: 'test_seller', + email: 'test@example.com', + profile: { onboarding_step: 4, phone_verified: true, email_verified: true, is_gstin_verified: true, aadhar_s3_key: 'mock_aadhar', pan_s3_key: 'mock_pan', store_name: 'Crafty Store', logo_s3_key: 'mock_logo', street: '123 Loom Lane', status: 'pending_approval' } + }; } else if (url.includes('/api/products')) { responseData = []; } else if (url.includes('/api/orders')) { @@ -91,6 +113,7 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => { fireEvent.change(whatsappCodeInput, { target: { value: '123456' } }) const verifyWhatsappBtn = screen.getByRole('button', { name: /Verify Code/i }) fireEvent.click(verifyWhatsappBtn) + expect(await screen.findByText(/✓ Mobile & WhatsApp Verified/i)).toBeInTheDocument() const sendEmailBtn = screen.getByText(/Send Email OTP/i) fireEvent.click(sendEmailBtn) @@ -100,17 +123,19 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => { fireEvent.change(codeInputs[codeInputs.length - 1], { target: { value: '123456' } }) const verifyButtons = screen.getAllByRole('button', { name: /Verify Code/i }) fireEvent.click(verifyButtons[verifyButtons.length - 1]) + expect(await screen.findByText(/✓ Email Verified/i)).toBeInTheDocument() const nextStep1Btn = screen.getByRole('button', { name: /Next Step: Identity Verification/i }) fireEvent.click(nextStep1Btn) // Step 2: Tax & Identity Verification - expect(screen.getByText(/Step 2 of 3: Tax & Identity Verification/i)).toBeInTheDocument() + expect(await screen.findByText(/Step 2 of 3: Tax & Identity Verification/i)).toBeInTheDocument() const taxInput = screen.getByLabelText(/GSTIN Number \*/i) fireEvent.change(taxInput, { target: { value: '29AAAAA1111A1Z1' } }) const verifyGstinBtn = screen.getByRole('button', { name: /Submit GSTIN/i }) fireEvent.click(verifyGstinBtn) + expect(await screen.findByText(/verification will be completed within next 24 hrs/i)).toBeInTheDocument() // Upload files const aadharInput = screen.getByLabelText(/Aadhaar Card Upload \*/i) @@ -118,13 +143,16 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => { // Trigger file changes fireEvent.change(aadharInput, { target: { files: [{ name: 'aadhar_card.pdf' }] } }) + expect(await screen.findByText(/Selected: aadhar_card.pdf/i)).toBeInTheDocument() + fireEvent.change(panInput, { target: { files: [{ name: 'pan_card.pdf' }] } }) + expect(await screen.findByText(/Selected: pan_card.pdf/i)).toBeInTheDocument() const nextStep2Btn = screen.getByRole('button', { name: /Next Step: Store & Location/i }) fireEvent.click(nextStep2Btn) // Step 3: Store Details & Location Map - expect(screen.getByText(/Step 3 of 3: Store & Pickup Location/i)).toBeInTheDocument() + expect(await screen.findByText(/Step 3 of 3: Store & Pickup Location/i)).toBeInTheDocument() const storeNameInput = screen.getByLabelText(/Store Display Name \*/i) fireEvent.change(storeNameInput, { target: { value: 'Crafty Store' } }) @@ -144,7 +172,7 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => { fireEvent.click(submitBtn) // Welcome Page & Setup Tour (Account Under Review) - expect(screen.getByText(/Supplier Account Under Review/i)).toBeInTheDocument() + expect(await screen.findByText(/Supplier Account Under Review/i)).toBeInTheDocument() // Complete tour steps const startTourBtn = screen.getByRole('button', { name: /Start Guided Tour/i }) diff --git a/src/App.tsx b/src/App.tsx index 4c72f80..3caa432 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -65,10 +65,13 @@ export default function App() { const [isGstinVerified, setIsGstinVerified] = useState(false) const [aadharFile, setAadharFile] = useState(null) const [panFile, setPanFile] = useState(null) + const [aadharS3Key, setAadharS3Key] = useState(null) + const [panS3Key, setPanS3Key] = useState(null) // Step 3: Store and Location details const [storeName, setStoreName] = useState('My Artisan Handloom') const [storeLogo, setStoreLogo] = useState(null) + const [logoS3Key, setLogoS3Key] = useState(null) const [businessBio, setBusinessBio] = useState('Traditional weaving and local sustainable designs.') const [address, setAddress] = useState({ street: '123 Handloom Lane', @@ -140,6 +143,59 @@ export default function App() { const [withdrawAmount, setWithdrawAmount] = useState('') // Load data from backend on mount or when profile is complete / logged in + useEffect(() => { + const token = localStorage.getItem('access_token'); + if (token) { + apiFetch(`${CONFIG.apiBaseUrl}/api/profile/`) + .then(res => { + if (res.ok) return res.json(); + throw new Error('Session expired'); + }) + .then(user => { + if (user && user.profile) { + const profile = user.profile; + setPhone(profile.phone || ''); + setPhoneVerified(profile.phone_verified || false); + setEmail(user.email || ''); + setEmailVerified(profile.email_verified || false); + setGstin(profile.gstin || '29AAAAA1111A1Z1'); + setIsGstinVerified(profile.is_gstin_verified || false); + setAadharFile(profile.aadhar_file || null); + setPanFile(profile.pan_file || null); + setAadharS3Key(profile.aadhar_s3_key || null); + setPanS3Key(profile.pan_s3_key || null); + setStoreName(profile.store_name || 'My Artisan Handloom'); + setStoreLogo(profile.store_logo || null); + setLogoS3Key(profile.logo_s3_key || null); + setBusinessBio(profile.business_bio || 'Traditional weaving and local sustainable designs.'); + setAddress({ + street: profile.street || '123 Handloom Lane', + city: profile.city || 'Textile Town', + state: profile.state || 'Karnataka', + pincode: profile.pincode || '560001' + }); + if (profile.latitude && profile.longitude) { + setMapCoordinates({ lat: Number(profile.latitude), lng: Number(profile.longitude) }); + } + + const step = profile.onboarding_step; + if (step >= 4) { + setIsProfileComplete(true); + setCurrentPage('dashboard'); + } else { + setProfileStep(step); + setIsProfileComplete(false); + setCurrentPage('profile-completion'); + } + } + }) + .catch(err => { + console.error('Session restore failed:', err); + localStorage.removeItem('access_token'); + }); + } + }, []); + useEffect(() => { if (isProfileComplete || currentPage === 'dashboard') { // Fetch Products @@ -176,6 +232,30 @@ export default function App() { } }, [isProfileComplete, currentPage]); + const uploadDocument = async (file: File, fileType: string) => { + try { + const response = await apiFetch(`${CONFIG.apiBaseUrl}/api/profile/presigned-url/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ file_type: fileType, content_type: file.type }) + }); + if (!response.ok) throw new Error('Failed to get presigned URL'); + const data = await response.json(); + + // Simulate file upload PUT to mock presigned_url + await fetch(data.presigned_url, { + method: 'PUT', + body: file, + headers: { 'Content-Type': file.type } + }).catch(err => console.log('Mock S3 upload:', err)); + + return data.s3_key; + } catch (err) { + console.error(err); + return `suppliers/default/${fileType}.jpg`; + } + }; + const handleAcceptOrder = (id: string) => { apiFetch(`${CONFIG.apiBaseUrl}/api/orders/${id}/accept/`, { method: 'POST' @@ -206,12 +286,31 @@ export default function App() { // GSTIN verification simulator const handleVerifyGstin = () => { - if (!gstin.trim()) return - setIsGstinVerified(true) + if (!gstin.trim() || gstin.length !== 15) { + alert('Invalid GSTIN length. Must be 15 chars.'); + return; + } + apiFetch(`${CONFIG.apiBaseUrl}/api/profile/submit-gstin/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ gstin }) + }) + .then(res => { + if (!res.ok) throw new Error('Failed to verify GSTIN.'); + return res.json(); + }) + .then(data => { + if (data.verified) { + setIsGstinVerified(true); + } + }) + .catch(err => { + alert(err.message); + }); } // Handle Logo Upload simulation - const handleLogoChange = (e: React.ChangeEvent) => { + const handleLogoChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) { const reader = new FileReader() @@ -219,6 +318,9 @@ export default function App() { setStoreLogo(reader.result as string) } reader.readAsDataURL(file) + + const s3Key = await uploadDocument(file, 'logo'); + setLogoS3Key(s3Key); } } @@ -243,6 +345,7 @@ export default function App() { }) .catch(err => console.error('Error logging out:', err)) .finally(() => { + localStorage.removeItem('access_token') // Reset all onboarding & profile states to their defaults setIsProfileComplete(false) setProfileStep(1) @@ -263,9 +366,12 @@ export default function App() { setIsGstinVerified(false) setAadharFile(null) setPanFile(null) + setAadharS3Key(null) + setPanS3Key(null) setStoreName('My Artisan Handloom') setStoreLogo(null) + setLogoS3Key(null) setBusinessBio('Traditional weaving and local sustainable designs.') setAddress({ street: '123 Handloom Lane', @@ -320,7 +426,9 @@ export default function App() { username: email, email: email, phone: phone, - password: password + password: password, + confirm_password: confirmPassword, + policy_accepted: policyAccepted }) }) .then(res => { @@ -334,7 +442,12 @@ export default function App() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: email, password: password }) }) - .then(() => { + .then(res => { + if (!res.ok) throw new Error('Auto-login failed.'); + return res.json(); + }) + .then(data => { + localStorage.setItem('access_token', data.access_token); navigateTo('profile-completion') }) }) @@ -362,26 +475,104 @@ export default function App() { if (!res.ok) throw new Error('Invalid credentials.'); return res.json(); }) - .then(() => { - setIsProfileComplete(true) - navigateTo('dashboard', true) + .then(data => { + localStorage.setItem('access_token', data.access_token); + if (data.user && data.user.profile) { + const profile = data.user.profile; + setPhone(profile.phone || ''); + setPhoneVerified(profile.phone_verified || false); + setEmail(data.user.email || ''); + setEmailVerified(profile.email_verified || false); + setGstin(profile.gstin || '29AAAAA1111A1Z1'); + setIsGstinVerified(profile.is_gstin_verified || false); + setAadharFile(profile.aadhar_file || null); + setPanFile(profile.pan_file || null); + setAadharS3Key(profile.aadhar_s3_key || null); + setPanS3Key(profile.pan_s3_key || null); + setStoreName(profile.store_name || 'My Artisan Handloom'); + setStoreLogo(profile.store_logo || null); + setLogoS3Key(profile.logo_s3_key || null); + setBusinessBio(profile.business_bio || 'Traditional weaving and local sustainable designs.'); + setAddress({ + street: profile.street || '123 Handloom Lane', + city: profile.city || 'Textile Town', + state: profile.state || 'Karnataka', + pincode: profile.pincode || '560001' + }); + if (profile.latitude && profile.longitude) { + setMapCoordinates({ lat: Number(profile.latitude), lng: Number(profile.longitude) }); + } + + const step = profile.onboarding_step; + if (step >= 4) { + setIsProfileComplete(true); + navigateTo('dashboard', true); + } else { + setProfileStep(step); + setIsProfileComplete(false); + navigateTo('profile-completion'); + } + } else { + setIsProfileComplete(false); + navigateTo('profile-completion'); + } }) .catch(err => { alert(err.message); }); } + const saveProfileBackend = (isComplete: boolean) => { + return apiFetch(`${CONFIG.apiBaseUrl}/api/profile/`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + phone: phone, + phone_verified: phoneVerified, + email_verified: emailVerified, + gstin: gstin, + is_gstin_verified: isGstinVerified, + aadhar_file: aadharFile, + pan_file: panFile, + aadhar_s3_key: aadharS3Key || 'suppliers/default/aadhar.pdf', + pan_s3_key: panS3Key || 'suppliers/default/pan.pdf', + store_name: storeName, + store_logo: storeLogo || 'https://images.unsplash.com/photo-1513519245088-0e12902e5a38?auto=format&fit=crop&q=80&w=800', + logo_s3_key: logoS3Key || 'suppliers/default/logo.jpg', + business_bio: businessBio, + street: address.street, + city: address.city, + state: address.state, + pincode: address.pincode, + latitude: mapCoordinates.lat, + longitude: mapCoordinates.lng, + is_profile_complete: isComplete + }) + }) + .then(res => { + if (!res.ok) throw new Error('Failed to update profile.'); + return res.json(); + }); + }; + const handleProfileSubmit = (e: React.FormEvent) => { e.preventDefault() if (profileStep === 2) { setIsGstinVerified(true) } - if (profileStep < 3) { - setProfileStep(prev => prev + 1) - } else { - setIsProfileComplete(true) - navigateTo('welcome-tour', true) - } + const isLastStep = profileStep === 3; + saveProfileBackend(isLastStep) + .then(() => { + if (!isLastStep) { + setProfileStep(prev => prev + 1) + } else { + setIsProfileComplete(true) + navigateTo('welcome-tour', true) + } + }) + .catch(err => { + alert(err.message); + }); } // --- Dashboard Logic Actions --- @@ -1107,12 +1298,26 @@ export default function App() { type="button" className="btn btn-dark" onClick={() => { - if (enteredPhoneOtp === '123456') { - setPhoneVerified(true); - alert('Phone verified successfully!'); - } else { - alert('Incorrect code. Enter 123456'); - } + apiFetch(`${CONFIG.apiBaseUrl}/api/auth/verify-otp/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'phone', otp: enteredPhoneOtp }) + }) + .then(res => { + if (!res.ok) throw new Error('Incorrect code. Enter 123456'); + return res.json(); + }) + .then(data => { + if (data.verified) { + setPhoneVerified(true); + alert('Phone verified successfully!'); + } else { + alert('Incorrect code. Enter 123456'); + } + }) + .catch(err => { + alert(err.message); + }); }} > Verify Code @@ -1165,12 +1370,26 @@ export default function App() { type="button" className="btn btn-dark" onClick={() => { - if (enteredEmailOtp === '123456') { - setEmailVerified(true); - alert('Email verified successfully!'); - } else { - alert('Incorrect code. Enter 123456'); - } + apiFetch(`${CONFIG.apiBaseUrl}/api/auth/verify-otp/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'email', otp: enteredEmailOtp }) + }) + .then(res => { + if (!res.ok) throw new Error('Incorrect code. Enter 123456'); + return res.json(); + }) + .then(data => { + if (data.verified) { + setEmailVerified(true); + alert('Email verified successfully!'); + } else { + alert('Incorrect code. Enter 123456'); + } + }) + .catch(err => { + alert(err.message); + }); }} > Verify Code @@ -1238,10 +1457,14 @@ export default function App() { id="aadhar-upload" type="file" accept=".pdf,image/*" - onChange={(e) => { + onChange={async (e) => { if (e.target.files?.[0]) { - setAadharFile(e.target.files[0].name); - alert(`Aadhaar card uploaded: ${e.target.files[0].name}`); + const file = e.target.files[0]; + setAadharFile(file.name); + alert(`Aadhaar card selected: ${file.name}. Uploading...`); + const s3Key = await uploadDocument(file, 'aadhar'); + setAadharS3Key(s3Key); + alert(`Aadhaar card uploaded successfully!`); } }} style={{ display: 'none' }} @@ -1267,10 +1490,14 @@ export default function App() { id="pan-upload" type="file" accept=".pdf,image/*" - onChange={(e) => { + onChange={async (e) => { if (e.target.files?.[0]) { - setPanFile(e.target.files[0].name); - alert(`PAN card uploaded: ${e.target.files[0].name}`); + const file = e.target.files[0]; + setPanFile(file.name); + alert(`PAN card selected: ${file.name}. Uploading...`); + const s3Key = await uploadDocument(file, 'pan'); + setPanS3Key(s3Key); + alert(`PAN card uploaded successfully!`); } }} style={{ display: 'none' }} diff --git a/src/config.ts b/src/config.ts index aef9f3b..87713cc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -121,8 +121,14 @@ export const CONFIG = { } export function apiFetch(url: string, options: RequestInit = {}): Promise { + const token = localStorage.getItem('access_token'); + const headers = { + ...options.headers, + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; return fetch(url, { ...options, + headers, credentials: 'include' }); }