feat: architecture refactor — core layer, spreadsheet engine, envelope bidding, tests
CI / quality (push) Waiting to run

- Add src/core/ (config, errors, events, http, observability, storage, query)
- Add src/Spreadsheet/ engine with native POI backend and Excel bridge
- Add Envelope C spreadsheet flow and tender bidding envelope modules
- Consolidate components into src/components/, remove legacy Component/ and Hooks/
- Add registration state/validation, auth session guard, profile edit modules
- Add unit tests across axios, redux, bidding, registration, encryption, core
- Add CI workflow, docs (architecture, audit, DSC API spec), keystore example
- Remove tracked keystore binary; ignore scripts.zip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abin Toppo
2026-07-16 10:20:22 +05:30
co-authored by Claude Fable 5
parent 9acec328a7
commit 1d7646d938
217 changed files with 11068 additions and 6855 deletions
@@ -0,0 +1,109 @@
/**
* EnvelopeCSpreadsheetScreen — full-screen native BOQ editor for Envelope-C.
*
* Opens the tender's BOQ template, renders the Excel-like editor (edit only the
* originally-editable cells, live recalculation, validation), and submits the
* metadata-preserving workbook through the existing Envelope-C API.
*
* Route params: { tender: <proceedToBid tender data>, buttonEnable?: boolean }.
*/
import React from 'react';
import { ActivityIndicator, KeyboardAvoidingView, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native';
import { FileUp, Send } from 'lucide-react-native';
import InsideHeader from '../../../components/InsideHeader';
import AppText from '../../../components/AppText';
import { useTheme } from '../../../Hooks/useTheme';
import SpreadsheetView from '../../../Spreadsheet/components/SpreadsheetView';
import useEnvelopeCWorkbook from './useEnvelopeCWorkbook';
const EnvelopeCSpreadsheetScreen: React.FC = () => {
const route = useRoute<any>();
const navigation = useNavigation<any>();
const { colors, spacing } = useTheme();
const tender = route.params?.tender;
const buttonEnable = route.params?.buttonEnable ?? true;
const { ss, submitting, submit, importFromDevice } = useEnvelopeCWorkbook(tender, () =>
navigation.goBack(),
);
const canSubmit = ss.status === 'ready' && !submitting && ss.errorCount === 0;
return (
<View style={{ flex: 1, backgroundColor: colors.background }}>
<InsideHeader title="Envelope C" showArrow />
{/* Keep the grid + footer above the keyboard. Android already resizes the
window (adjustResize); iOS needs padding behaviour. */}
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={0}
>
<View style={{ flex: 1 }}>
<SpreadsheetView ss={ss} emptyHint="Loading the BOQ template…" />
</View>
<View style={[styles.footer, { backgroundColor: colors.surface, borderTopColor: colors.border }]}>
{buttonEnable && (
<TouchableOpacity
style={[
styles.primaryBtn,
{ backgroundColor: canSubmit ? colors.primary : colors.secondary, marginLeft: spacing.s },
]}
onPress={submit}
disabled={!canSubmit}
activeOpacity={0.85}
>
{submitting ? (
<ActivityIndicator size="small" color={colors.buttonText} />
) : (
<>
<Send size={16} color={colors.buttonText} />
<AppText variant="button" color={colors.buttonText}>
{ss.errorCount > 0 ? `Fix ${ss.errorCount} error(s)` : 'Submit'}
</AppText>
</>
)}
</TouchableOpacity>
)}
</View>
</KeyboardAvoidingView>
</View>
);
};
const styles = StyleSheet.create({
footer: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
borderTopWidth: StyleSheet.hairlineWidth,
},
secondaryBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 12,
borderWidth: 1.5,
},
primaryBtn: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 14,
borderRadius: 12,
},
});
export default EnvelopeCSpreadsheetScreen;
@@ -0,0 +1,192 @@
/**
* useEnvelopeCWorkbook — Envelope-C BOQ integration.
*
* Implements the mobile-bid encryption contract in MOBILE_BID_DSC_API_SPEC.md:
* downloads the (plaintext) BOQ template into the editor, then on submit
* 1. exports the metadata-preserving workbook bytes,
* 2. AES/ECB encrypts the RAW file bytes with a SHA-1(K)-derived key,
* 3. RSA-wraps the envelope key K with each officer's public key,
* 4. uploads the raw ciphertext file + `randomKeyGenerated=K` + wrapped keys.
*
* Steps 2–3 are exactly what makes web rebid/opening able to RSA-unwrap the
* key; without them the stored `contractor_enc_key` is a raw key and the DSC
* unwrap times out.
*/
import { useCallback, useEffect, useState } from 'react';
import { Alert } from 'react-native';
import axios from 'axios';
import { useSelector } from 'react-redux';
import useSpreadsheet from '../../../Spreadsheet/hooks/useSpreadsheet';
import FileManager from '../../../Spreadsheet/engine/FileManager';
import {
encrypt,
encryptFileBytesToBase64,
encryptWithRSA,
sha256HexOfBytes,
} from '../../../Services/Encryption/encrypt';
import { GET_ENVELOPE_C, SUBMIT_ENVELOPE_C_UPLOAD } from '../../../Services/ApiCall/ApiUrls';
interface OfficerKey {
officersId: number;
officerPublicKey: string;
officerType: string;
}
interface SaveEnvelopeOfficerDetail {
officerId: number;
officerEncryptedKey: string;
officerType: string;
}
const ENCRYPTED_FILE_NAME = 'envelopeC_encrypted.xlsx';
export const useEnvelopeCWorkbook = (tender: any, onSubmitted?: () => void) => {
const ss = useSpreadsheet();
const tokenObj = useSelector((state: any) => state.token?.token);
const bearer = tokenObj?.token;
const [submitting, setSubmitting] = useState(false);
const tenderId = tender?.tenderId;
const encryptionKey: string = tender?.envelopeKeyDetailsResDTO?.envelopeCGenerateKey || '';
/* ------------------------------- load ------------------------------- */
const loadFromApi = useCallback(async () => {
if (!tenderId) {
Alert.alert('Error', 'Missing tender id.');
return;
}
try {
const response = await axios.get(`${GET_ENVELOPE_C}${tenderId}`, {
responseType: 'arraybuffer',
headers: { Authentication: `Bearer ${bearer}` },
});
await ss.loadFromBytes(response.data as ArrayBuffer, `envelopeC_${tenderId}.xlsx`);
} catch (e: any) {
Alert.alert('Import failed', e?.response?.data?.message || e?.message || 'Could not load the BOQ template.');
}
// ss identity changes across renders; loadFromBytes is stable.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tenderId, bearer]);
useEffect(() => {
loadFromApi();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadFromApi]);
/* --------------------------- import from device --------------------- */
const importFromDevice = useCallback(async () => {
try {
const picked = await FileManager.pickWorkbook();
if (!picked) return;
await ss.loadFromBytes(picked.bytes, picked.name);
} catch (e: any) {
Alert.alert('Import failed', e?.message || 'Could not open the selected file.');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/* ------------------------------- submit ----------------------------- */
const buildOfficerEncryptedKeys = useCallback((): SaveEnvelopeOfficerDetail[] | null => {
const officerKeys: OfficerKey[] = tender?.envelopeKeyDetailsResDTO?.departmentOfficerPublicKeys || [];
// RSA/ECB/PKCS1-wrap the plaintext envelope key K with each officer's public
// key (Base64 SPKI DER → ~344-char Base64 ciphertext), per
// MOBILE_BID_DSC_API_SPEC.md. The web DSC applet unwraps it with the
// officer's PRIVATE key during rebid/opening — a raw or empty value makes
// the bid unrecoverable, so any wrap failure aborts the submission.
// (`officersId` -> `officerId` per the API contract.)
const wrapped: SaveEnvelopeOfficerDetail[] = [];
for (const o of officerKeys) {
const officerEncryptedKey = encryptWithRSA(encryptionKey, o.officerPublicKey);
if (!officerEncryptedKey) return null;
wrapped.push({
officerId: o.officersId,
officerEncryptedKey,
officerType: o.officerType,
});
}
return wrapped;
}, [tender, encryptionKey]);
const submit = useCallback(async () => {
// 1) Validation gate — block submission while any editable cell is invalid.
const errors = ss.validateAll();
if (errors.length > 0) {
Alert.alert('Validation failed', `Please fix ${errors.length} highlighted cell(s) before submitting.`);
return;
}
if (!encryptionKey) {
// Matches the server's HTTP 400 guard: a mobile bid must be client-
// encrypted and echo randomKeyGenerated.
Alert.alert('Error', 'Encryption key not available for this tender.');
return;
}
setSubmitting(true);
let encryptedPath: string | null = null;
try {
// 2) RSA-wrap the envelope key K for every opening officer + contractor.
const officerKeys = buildOfficerEncryptedKeys();
if (!officerKeys) {
throw new Error('Could not encrypt the envelope key for the opening officers.');
}
// 3) Export the metadata-preserving workbook and AES-encrypt the RAW
// bytes with the SHA-1(K) file key; upload as raw ciphertext bytes.
const bytes = await ss.exportBytes();
const cipherBase64 = encryptFileBytesToBase64(bytes, encryptionKey);
if (!cipherBase64) throw new Error('Failed to encrypt the workbook.');
encryptedPath = await FileManager.writeBase64(ENCRYPTED_FILE_NAME, cipherBase64);
const hashValue = sha256HexOfBytes(bytes);
// 4) Build the request exactly as MOBILE_BID_DSC_API_SPEC.md specifies.
const requestDto = {
tenderId,
deptId: tender?.deptId,
typesOfTender: tender?.typesOfTender,
deviceType: 'Mobile',
randomKeyGenerated: encryptionKey, // plaintext K, echoed back
// The financial value lives inside the encrypted BOQ file; the text
// fields still travel as AES(text) ciphertext per the contract.
fieldValue: '',
fieldValueInWord: encrypt('zero', encryptionKey) || '',
hashValue,
fileName: 'boq.xlsx',
saveEnvelopesOfficersDetailsResDTO: officerKeys,
};
const formData = new FormData();
formData.append('requestDto', JSON.stringify(requestDto));
formData.append('file', {
uri: `file://${encryptedPath}`,
name: 'boq.xlsx',
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
} as any);
// 5) Upload.
await axios.post(SUBMIT_ENVELOPE_C_UPLOAD, formData, {
headers: { 'Content-Type': 'multipart/form-data', Authentication: `Bearer ${bearer}` },
});
Alert.alert('Success', 'Envelope-C BOQ submitted successfully.');
onSubmitted?.();
} catch (e: any) {
Alert.alert('Save failed', e?.response?.data?.message || e?.message || 'Could not submit the workbook.');
} finally {
if (encryptedPath) await FileManager.remove(encryptedPath);
setSubmitting(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ss, encryptionKey, tenderId, tender, bearer, buildOfficerEncryptedKeys, onSubmitted]);
return { ss, submitting, submit, importFromDevice, reload: loadFromApi };
};
export default useEnvelopeCWorkbook;