diff --git a/backend/api/v1/__init__.py b/backend/api/v1/__init__.py index 748781c3..dae38337 100644 --- a/backend/api/v1/__init__.py +++ b/backend/api/v1/__init__.py @@ -11,6 +11,7 @@ from .market import market_bp from .risk import risk_bp from .schemes import schemes_bp +from .government_schemes import gov_schemes_bp from .weather import weather_bp from .traceability import traceability_bp from .disease import disease_bp @@ -109,6 +110,7 @@ api_v1.register_blueprint(spatial_yield_bp, url_prefix="/spatial-yield") api_v1.register_blueprint(carbon_bp, url_prefix="/carbon") api_v1.register_blueprint(carbon_v2_bp, url_prefix="/carbon-v2") +api_v1.register_blueprint(gov_schemes_bp, url_prefix="/government-schemes") api_v1.register_blueprint(advisory_bp, url_prefix="/crop-advisory") api_v1.register_blueprint(gews_bp, url_prefix='/gews') api_v1.register_blueprint(transparency_bp, url_prefix='/transparency') diff --git a/backend/api/v1/government_schemes.py b/backend/api/v1/government_schemes.py new file mode 100644 index 00000000..959a4c15 --- /dev/null +++ b/backend/api/v1/government_schemes.py @@ -0,0 +1,167 @@ +from flask import Blueprint, request, jsonify +from backend.services.government_scheme_service import government_scheme_service +from backend.models.government_schemes import GovernmentScheme, SchemeApplication +from backend.extensions import db +from datetime import datetime + +gov_schemes_bp = Blueprint("gov_schemes", __name__) + + +@gov_schemes_bp.route("/government-schemes", methods=["GET"]) +def get_schemes(): + filters = { + "ministry": request.args.get("ministry"), + "region": request.args.get("region"), + "deadline_from": datetime.strptime( + request.args.get("deadline_from"), "%Y-%m-%d" + ).date() + if request.args.get("deadline_from") + else None, + "deadline_to": datetime.strptime( + request.args.get("deadline_to"), "%Y-%m-%d" + ).date() + if request.args.get("deadline_to") + else None, + } + filters = {k: v for k, v in filters.items() if v is not None} + + schemes = government_scheme_service.get_all_schemes(filters) + + return jsonify({"status": "success", "count": len(schemes), "data": schemes}), 200 + + +@gov_schemes_bp.route("/government-schemes/search", methods=["GET"]) +def search_schemes(): + keyword = request.args.get("q", "") + if not keyword: + return jsonify({"status": "error", "message": "Search keyword required"}), 400 + + schemes = government_scheme_service.search_schemes(keyword) + + return jsonify({"status": "success", "count": len(schemes), "data": schemes}), 200 + + +@gov_schemes_bp.route("/government-schemes/", methods=["GET"]) +def get_scheme(scheme_id): + scheme = government_scheme_service.get_scheme_by_id(scheme_id) + + if not scheme: + return jsonify({"status": "error", "message": "Scheme not found"}), 404 + + return jsonify({"status": "success", "data": scheme}), 200 + + +@gov_schemes_bp.route( + "/government-schemes//eligibility", methods=["POST"] +) +def check_eligibility(scheme_id): + data = request.get_json() + user_id = data.get("user_id") + farm_id = data.get("farm_id") + + if not user_id: + return jsonify({"status": "error", "message": "User ID required"}), 400 + + eligibility = government_scheme_service.check_eligibility( + scheme_id, user_id, farm_id + ) + + return jsonify({"status": "success", "data": eligibility}), 200 + + +@gov_schemes_bp.route("/government-schemes//assistance", methods=["GET"]) +def get_assistance(scheme_id): + assistance = government_scheme_service.get_application_assistance(scheme_id) + + if not assistance: + return jsonify({"status": "error", "message": "Scheme not found"}), 404 + + return jsonify({"status": "success", "data": assistance}), 200 + + +@gov_schemes_bp.route("/government-schemes/applications", methods=["POST"]) +def create_application(): + data = request.get_json() + scheme_id = data.get("scheme_id") + user_id = data.get("user_id") + farm_id = data.get("farm_id") + application_data = data.get("application_data") + + if not scheme_id or not user_id: + return jsonify( + {"status": "error", "message": "Scheme ID and User ID required"} + ), 400 + + result = government_scheme_service.create_application( + scheme_id, user_id, farm_id, application_data + ) + + if result.get("success"): + return jsonify({"status": "success", "data": result}), 201 + else: + return jsonify({"status": "error", "message": result.get("message")}), 400 + + +@gov_schemes_bp.route( + "/government-schemes/applications//submit", methods=["POST"] +) +def submit_application(application_id): + result = government_scheme_service.submit_application(application_id) + + if result.get("success"): + return jsonify({"status": "success", "message": result.get("message")}), 200 + else: + return jsonify({"status": "error", "message": result.get("message")}), 400 + + +@gov_schemes_bp.route( + "/government-schemes/applications/user/", methods=["GET"] +) +def get_user_applications(user_id): + status = request.args.get("status") + + applications = government_scheme_service.get_user_applications(user_id, status) + + return jsonify( + {"status": "success", "count": len(applications), "data": applications} + ), 200 + + +@gov_schemes_bp.route( + "/government-schemes/applications/", methods=["GET"] +) +def get_application(application_id): + application = SchemeApplication.query.get(application_id) + + if not application: + return jsonify({"status": "error", "message": "Application not found"}), 404 + + return jsonify({"status": "success", "data": application.to_dict()}), 200 + + +@gov_schemes_bp.route("/government-schemes/deadlines", methods=["GET"]) +def get_upcoming_deadlines(): + days = request.args.get("days", 30, type=int) + + schemes = government_scheme_service.get_upcoming_deadlines(days) + + return jsonify({"status": "success", "count": len(schemes), "data": schemes}), 200 + + +@gov_schemes_bp.route("/government-schemes//reminders", methods=["POST"]) +def create_reminders(scheme_id): + data = request.get_json() + user_id = data.get("user_id") + + if not user_id: + return jsonify({"status": "error", "message": "User ID required"}), 400 + + reminders = government_scheme_service.create_deadline_reminders(scheme_id, user_id) + + return jsonify( + { + "status": "success", + "count": len(reminders), + "message": f"Created {len(reminders)} deadline reminders", + } + ), 201 diff --git a/backend/models/__init__.py b/backend/models/__init__.py index abd7a02a..721bfded 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -1,13 +1,27 @@ """ Database models package. """ + from .user import User, UserRole from .loan_request import LoanRequest from .prediction_history import PredictionHistory -from .misc import Notification, File, YieldPool, PoolContribution, ResourceShare, PoolVote +from .misc import ( + Notification, + File, + YieldPool, + PoolContribution, + ResourceShare, + PoolVote, +) from .gews import DiseaseIncident, OutbreakZone, OutbreakAlert from .traceability import SupplyBatch, CustodyLog, QualityGrade, BatchStatus -from .insurance import InsurancePolicy, ClaimRequest as LegacyClaim, RiskScoreHistory, DynamicPremiumLog, RiskFactorSnapshot +from .insurance import ( + InsurancePolicy, + ClaimRequest as LegacyClaim, + RiskScoreHistory, + DynamicPremiumLog, + RiskFactorSnapshot, +) from .forum import ForumCategory, ForumThread, PostComment, Upvote, UserReputation from .knowledge import Question, Answer, KnowledgeVote, Badge, UserBadge, UserExpertise from .equipment import Equipment, RentalBooking, AvailabilityCalendar, PaymentEscrow @@ -16,22 +30,66 @@ from .audit_log import AuditLog, UserSession from .media_payload import MediaPayload from .weather import ( - WeatherData, CropAdvisory, AdvisorySubscription, RiskTrigger, - ClimateTelemetryEvent, ForceMajeureAlert, ParametricPolicyTrigger + WeatherData, + CropAdvisory, + AdvisorySubscription, + RiskTrigger, + ClimateTelemetryEvent, + ForceMajeureAlert, + ParametricPolicyTrigger, ) from .weather import WeatherData, CropAdvisory, AdvisorySubscription, RiskTrigger from .sustainability import ( - CarbonPractice, CreditLedger, AuditRequest, CarbonLedger, - EmissionSource, SustainabilityScore, ESGMarketListing + CarbonPractice, + CreditLedger, + AuditRequest, + CarbonLedger, + EmissionSource, + SustainabilityScore, + ESGMarketListing, ) from .vendor_profile import VendorProfile from .procurement import ProcurementItem, BulkOrder, OrderEvent -from .irrigation import IrrigationZone, SensorLog, ValveStatus, IrrigationSchedule, AquiferLevel, WaterRightsQuota -from .processing import ProcessingBatch, StageLog, QualityCheck, ProcessingStage, SpectralScanData, DynamicGradeAdjustment -from .insurance_v2 import CropPolicy, ClaimRequest, PayoutLedger, AdjusterNote, ParametricAutoSettlement +from .irrigation import ( + IrrigationZone, + SensorLog, + ValveStatus, + IrrigationSchedule, + AquiferLevel, + WaterRightsQuota, +) +from .processing import ( + ProcessingBatch, + StageLog, + QualityCheck, + ProcessingStage, + SpectralScanData, + DynamicGradeAdjustment, +) +from .insurance_v2 import ( + CropPolicy, + ClaimRequest, + PayoutLedger, + AdjusterNote, + ParametricAutoSettlement, +) from .insurance_v2 import CropPolicy, ClaimRequest, PayoutLedger, AdjusterNote -from .machinery import EngineHourLog, MaintenanceCycle, DamageReport, RepairOrder, AssetValueSnapshot, ComponentWearMap, MaintenanceEscrow -from .soil_health import SoilTest, FertilizerRecommendation, ApplicationLog, RegenerativeFarmingLog, CarbonMintEvent +from .machinery import ( + EngineHourLog, + MaintenanceCycle, + DamageReport, + RepairOrder, + AssetValueSnapshot, + ComponentWearMap, + MaintenanceEscrow, +) +from .soil_health import ( + SoilTest, + FertilizerRecommendation, + ApplicationLog, + RegenerativeFarmingLog, + CarbonMintEvent, +) from .loan_v2 import RepaymentSchedule, PaymentHistory, DefaultRiskScore, CollectionNote from .warehouse import WarehouseLocation, StockItem, StockMovement, ReconciliationLog from .climate import ClimateZone, SensorNode, TelemetryLog, AutomationTrigger @@ -39,8 +97,14 @@ from .logistics_v2 import DriverProfile, DeliveryVehicle, TransportRoute, FuelLog from .labor import WorkerProfile, WorkShift, HarvestLog, PayrollEntry from .logistics_v2 import ( - DriverProfile, DeliveryVehicle, TransportRoute, FuelLog, - PhytoSanitaryCertificate, FreightEscrow, CustomsCheckpoint, GPSTelemetry + DriverProfile, + DeliveryVehicle, + TransportRoute, + FuelLog, + PhytoSanitaryCertificate, + FreightEscrow, + CustomsCheckpoint, + GPSTelemetry, ) from .transparency import ProduceReview, PriceAdjustmentLog from .barter import BarterTransaction, BarterResource, ResourceValueIndex @@ -53,84 +117,212 @@ from .precision_irrigation import WaterStressIndex, IrrigationValveAutomation, AquiferMonitoring from .disease import MigrationVector, ContainmentZone from .ledger import ( - LedgerAccount, LedgerTransaction, LedgerEntry, - FXValuationSnapshot, Vault, VaultCurrencyPosition, FXRate, - AccountType, EntryType, TransactionType + LedgerAccount, + LedgerTransaction, + LedgerEntry, + FXValuationSnapshot, + Vault, + VaultCurrencyPosition, + FXRate, + AccountType, + EntryType, + TransactionType, ) +from .government_schemes import GovernmentScheme, SchemeApplication, SchemeReminder __all__ = [ # Core - 'User', 'UserRole', 'LoanRequest', 'PredictionHistory', - 'Notification', 'File', 'YieldPool', 'PoolContribution', - 'ResourceShare', 'PoolVote', + "User", + "UserRole", + "LoanRequest", + "PredictionHistory", + "Notification", + "File", + "YieldPool", + "PoolContribution", + "ResourceShare", + "PoolVote", # Disease & Outbreak - 'DiseaseIncident', 'OutbreakZone', 'OutbreakAlert', - 'MigrationVector', 'ContainmentZone', + "DiseaseIncident", + "OutbreakZone", + "OutbreakAlert", + "MigrationVector", + "ContainmentZone", # Traceability - 'SupplyBatch', 'CustodyLog', 'QualityGrade', 'BatchStatus', + "SupplyBatch", + "CustodyLog", + "QualityGrade", + "BatchStatus", # Insurance - 'InsurancePolicy', 'LegacyClaim', 'RiskScoreHistory', 'DynamicPremiumLog', 'RiskFactorSnapshot', - 'CropPolicy', 'ClaimRequest', 'PayoutLedger', 'AdjusterNote', 'ParametricAutoSettlement', - 'CropPolicy', 'ClaimRequest', 'PayoutLedger', 'AdjusterNote', + "InsurancePolicy", + "LegacyClaim", + "RiskScoreHistory", + "DynamicPremiumLog", + "RiskFactorSnapshot", + "CropPolicy", + "ClaimRequest", + "PayoutLedger", + "AdjusterNote", + "ParametricAutoSettlement", + "CropPolicy", + "ClaimRequest", + "PayoutLedger", + "AdjusterNote", # Community - 'ForumCategory', 'ForumThread', 'PostComment', 'Upvote', 'UserReputation', - 'Question', 'Answer', 'KnowledgeVote', 'Badge', 'UserBadge', 'UserExpertise', + "ForumCategory", + "ForumThread", + "PostComment", + "Upvote", + "UserReputation", + "Question", + "Answer", + "KnowledgeVote", + "Badge", + "UserBadge", + "UserExpertise", # Equipment & Rental - 'Equipment', 'RentalBooking', 'AvailabilityCalendar', 'PaymentEscrow', + "Equipment", + "RentalBooking", + "AvailabilityCalendar", + "PaymentEscrow", # Farm - 'Farm', 'FarmMember', 'FarmAsset', 'FarmRole', + "Farm", + "FarmMember", + "FarmAsset", + "FarmRole", # Alerts - 'Alert', 'AlertPreference', + "Alert", + "AlertPreference", # Audit - 'AuditLog', 'UserSession', 'MediaPayload', + "AuditLog", + "UserSession", + "MediaPayload", # Weather - 'WeatherData', 'CropAdvisory', 'AdvisorySubscription', 'RiskTrigger', - 'ClimateTelemetryEvent', 'ForceMajeureAlert', 'ParametricPolicyTrigger', + "WeatherData", + "CropAdvisory", + "AdvisorySubscription", + "RiskTrigger", + "ClimateTelemetryEvent", + "ForceMajeureAlert", + "ParametricPolicyTrigger", # Sustainability & ESG - 'CarbonPractice', 'CreditLedger', 'AuditRequest', 'CarbonLedger', 'EmissionSource', - 'SustainabilityScore', 'ESGMarketListing', + "CarbonPractice", + "CreditLedger", + "AuditRequest", + "CarbonLedger", + "EmissionSource", + "SustainabilityScore", + "ESGMarketListing", # Procurement - 'VendorProfile', 'ProcurementItem', 'BulkOrder', 'OrderEvent', + "VendorProfile", + "ProcurementItem", + "BulkOrder", + "OrderEvent", # Irrigation & Water - 'IrrigationZone', 'SensorLog', 'ValveStatus', 'IrrigationSchedule', - 'AquiferLevel', 'WaterRightsQuota', + "IrrigationZone", + "SensorLog", + "ValveStatus", + "IrrigationSchedule", + "AquiferLevel", + "WaterRightsQuota", # Processing & Grading - 'ProcessingBatch', 'StageLog', 'QualityCheck', 'ProcessingStage', - 'SpectralScanData', 'DynamicGradeAdjustment', + "ProcessingBatch", + "StageLog", + "QualityCheck", + "ProcessingStage", + "SpectralScanData", + "DynamicGradeAdjustment", # Machinery - 'EngineHourLog', 'MaintenanceCycle', 'DamageReport', 'RepairOrder', - 'AssetValueSnapshot', 'ComponentWearMap', 'MaintenanceEscrow', + "EngineHourLog", + "MaintenanceCycle", + "DamageReport", + "RepairOrder", + "AssetValueSnapshot", + "ComponentWearMap", + "MaintenanceEscrow", # Soil & Carbon Sequestration - 'SoilTest', 'FertilizerRecommendation', 'ApplicationLog', - 'RegenerativeFarmingLog', 'CarbonMintEvent', + "SoilTest", + "FertilizerRecommendation", + "ApplicationLog", + "RegenerativeFarmingLog", + "CarbonMintEvent", # Finance - 'RepaymentSchedule', 'PaymentHistory', 'DefaultRiskScore', 'CollectionNote', - 'FarmBalanceSheet', 'SolvencySnapshot', 'ProfitabilityIndex', + "RepaymentSchedule", + "PaymentHistory", + "DefaultRiskScore", + "CollectionNote", + "FarmBalanceSheet", + "SolvencySnapshot", + "ProfitabilityIndex", # Warehouse - 'WarehouseLocation', 'StockItem', 'StockMovement', 'ReconciliationLog', + "WarehouseLocation", + "StockItem", + "StockMovement", + "ReconciliationLog", # Climate - 'ClimateZone', 'SensorNode', 'TelemetryLog', 'AutomationTrigger', + "ClimateZone", + "SensorNode", + "TelemetryLog", + "AutomationTrigger", # Labor - 'WorkerProfile', 'WorkShift', 'HarvestLog', 'PayrollEntry', 'LaborROIHistory', + "WorkerProfile", + "WorkShift", + "HarvestLog", + "PayrollEntry", + "LaborROIHistory", # Logistics - 'DriverProfile', 'DeliveryVehicle', 'TransportRoute', 'FuelLog', - 'PhytoSanitaryCertificate', 'FreightEscrow', 'CustomsCheckpoint', 'GPSTelemetry', + "DriverProfile", + "DeliveryVehicle", + "TransportRoute", + "FuelLog", + "PhytoSanitaryCertificate", + "FreightEscrow", + "CustomsCheckpoint", + "GPSTelemetry", # Transparency & Barter # Transparency & Barter - 'PhytoSanitaryCertificate', 'FreightEscrow', 'CustomsCheckpoint', 'GPSTelemetry', - 'Alert', 'AlertPreference', - 'AuditLog', 'UserSession', - 'MediaPayload', - 'ProduceReview', 'PriceAdjustmentLog', - 'BarterTransaction', 'BarterResource', 'ResourceValueIndex', + "PhytoSanitaryCertificate", + "FreightEscrow", + "CustomsCheckpoint", + "GPSTelemetry", + "Alert", + "AlertPreference", + "AuditLog", + "UserSession", + "MediaPayload", + "ProduceReview", + "PriceAdjustmentLog", + "BarterTransaction", + "BarterResource", + "ResourceValueIndex", # Reliability & Market - 'ReliabilityLog', 'ForwardContract', 'PriceHedgingLog', + "ReliabilityLog", + "ForwardContract", + "PriceHedgingLog", # Circular Economy & Biomass Energy + "WasteInventory", + "BioEnergyOutput", + "CircularCredit", + # Carbon Trading Escrow (L3-1642) + "CarbonTradeEscrow", + "EscrowAuditLog", + "CarbonCreditWallet", 'WasteInventory', 'BioEnergyOutput', 'CircularCredit', # Precision Irrigation (L3-1640) 'WaterStressIndex', 'IrrigationValveAutomation', 'AquiferMonitoring', # Double-Entry Ledger - 'LedgerAccount', 'LedgerTransaction', 'LedgerEntry', - 'FXValuationSnapshot', 'Vault', 'VaultCurrencyPosition', 'FXRate', - 'AccountType', 'EntryType', 'TransactionType', + "LedgerAccount", + "LedgerTransaction", + "LedgerEntry", + "FXValuationSnapshot", + "Vault", + "VaultCurrencyPosition", + "FXRate", + "AccountType", + "EntryType", + "TransactionType", + # Government Schemes + "GovernmentScheme", + "SchemeApplication", + "SchemeReminder", ] diff --git a/backend/models/government_schemes.py b/backend/models/government_schemes.py new file mode 100644 index 00000000..bce017da --- /dev/null +++ b/backend/models/government_schemes.py @@ -0,0 +1,200 @@ +from datetime import datetime, timedelta +from backend.extensions import db + + +class GovernmentScheme(db.Model): + __tablename__ = "government_schemes" + + id = db.Column(db.Integer, primary_key=True) + scheme_code = db.Column(db.String(50), unique=True, nullable=False, index=True) + name = db.Column(db.String(200), nullable=False) + ministry = db.Column(db.String(150)) + description = db.Column(db.Text) + + # Eligibility Criteria + farm_size_min = db.Column(db.Float) # Minimum farm size in acres + farm_size_max = db.Column(db.Float) # Maximum farm size in acres + income_limit = db.Column(db.Float) # Annual income limit in INR + required_crops = db.Column(db.Text) # JSON array of crop types + region_specific = db.Column(db.Boolean, default=False) + eligible_states = db.Column(db.Text) # JSON array of state names + + # Scheme Details + subsidy_percentage = db.Column(db.Float) + max_subsidy_amount = db.Column(db.Float) + application_fee = db.Column(db.Float, default=0) + + # Deadlines + application_start_date = db.Column(db.Date) + application_deadline = db.Column(db.Date) + + # Status + is_active = db.Column(db.Boolean, default=True) + + # Document Requirements + required_documents = db.Column(db.Text) # JSON array of document types + + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) + + applications = db.relationship( + "SchemeApplication", + backref="scheme", + lazy="dynamic", + cascade="all, delete-orphan", + ) + reminders = db.relationship( + "SchemeReminder", backref="scheme", lazy="dynamic", cascade="all, delete-orphan" + ) + + def to_dict(self): + import json + + return { + "id": self.id, + "scheme_code": self.scheme_code, + "name": self.name, + "ministry": self.ministry, + "description": self.description, + "eligibility": { + "farm_size_min": self.farm_size_min, + "farm_size_max": self.farm_size_max, + "income_limit": self.income_limit, + "required_crops": json.loads(self.required_crops) + if self.required_crops + else [], + "region_specific": self.region_specific, + "eligible_states": json.loads(self.eligible_states) + if self.eligible_states + else [], + }, + "benefits": { + "subsidy_percentage": self.subsidy_percentage, + "max_subsidy_amount": self.max_subsidy_amount, + "application_fee": self.application_fee, + }, + "deadlines": { + "start_date": self.application_start_date.isoformat() + if self.application_start_date + else None, + "deadline": self.application_deadline.isoformat() + if self.application_deadline + else None, + }, + "required_documents": json.loads(self.required_documents) + if self.required_documents + else [], + "is_active": self.is_active, + } + + +class SchemeApplication(db.Model): + __tablename__ = "scheme_applications" + + id = db.Column(db.Integer, primary_key=True) + scheme_id = db.Column( + db.Integer, db.ForeignKey("government_schemes.id"), nullable=False + ) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + farm_id = db.Column(db.Integer, db.ForeignKey("farms.id")) + + # Application Details + application_number = db.Column( + db.String(50), unique=True, nullable=False, index=True + ) + status = db.Column( + db.String(20), default="draft" + ) # draft, submitted, under_review, approved, rejected + + # Eligibility Check Results + eligibility_score = db.Column(db.Float) + eligibility_notes = db.Column(db.Text) + + # Application Data + farmer_details = db.Column(db.Text) # JSON + farm_details = db.Column(db.Text) # JSON + income_proof = db.Column(db.Text) # JSON with file reference + + # Documents + submitted_documents = db.Column( + db.Text + ) # JSON array of uploaded document references + documents_verified = db.Column(db.Boolean, default=False) + + # Assistance + assistance_notes = db.Column(db.Text) + assistance_contact = db.Column(db.String(100)) + + # Timeline + submitted_at = db.Column(db.DateTime) + reviewed_at = db.Column(db.DateTime) + approved_at = db.Column(db.DateTime) + rejection_reason = db.Column(db.Text) + + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) + + def to_dict(self): + import json + + return { + "id": self.id, + "scheme_id": self.scheme_id, + "application_number": self.application_number, + "status": self.status, + "eligibility_score": self.eligibility_score, + "eligibility_notes": self.eligibility_notes, + "submitted_documents": json.loads(self.submitted_documents) + if self.submitted_documents + else [], + "documents_verified": self.documents_verified, + "assistance_notes": self.assistance_notes, + "timeline": { + "submitted_at": self.submitted_at.isoformat() + if self.submitted_at + else None, + "reviewed_at": self.reviewed_at.isoformat() + if self.reviewed_at + else None, + "approved_at": self.approved_at.isoformat() + if self.approved_at + else None, + }, + "rejection_reason": self.rejection_reason, + } + + +class SchemeReminder(db.Model): + __tablename__ = "scheme_reminders" + + id = db.Column(db.Integer, primary_key=True) + scheme_id = db.Column( + db.Integer, db.ForeignKey("government_schemes.id"), nullable=False + ) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + + reminder_type = db.Column( + db.String(30) + ) # deadline_reminder, document_reminder, application_reminder + reminder_date = db.Column(db.DateTime, nullable=False) + message = db.Column(db.Text) + + is_sent = db.Column(db.Boolean, default=False) + sent_at = db.Column(db.DateTime) + + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + def to_dict(self): + return { + "id": self.id, + "scheme_id": self.scheme_id, + "reminder_type": self.reminder_type, + "reminder_date": self.reminder_date.isoformat(), + "message": self.message, + "is_sent": self.is_sent, + "sent_at": self.sent_at.isoformat() if self.sent_at else None, + } diff --git a/backend/services/government_scheme_service.py b/backend/services/government_scheme_service.py new file mode 100644 index 00000000..1b1c2b8a --- /dev/null +++ b/backend/services/government_scheme_service.py @@ -0,0 +1,282 @@ +from datetime import datetime, timedelta +from backend.models.government_schemes import ( + GovernmentScheme, + SchemeApplication, + SchemeReminder, +) +from backend.models.user import User +from backend.models.farm import Farm +from backend.extensions import db +import json + + +class GovernmentSchemeService: + def __init__(self): + pass + + def get_all_schemes(self, filters=None): + query = GovernmentScheme.query.filter_by(is_active=True) + + if filters: + if filters.get("ministry"): + query = query.filter(GovernmentScheme.ministry == filters["ministry"]) + if filters.get("region"): + state = filters.get("region") + query = query.filter( + db.or_( + GovernmentScheme.region_specific == False, + GovernmentScheme.eligible_states.like(f"%{state}%"), + ) + ) + if filters.get("deadline_from"): + query = query.filter( + GovernmentScheme.application_deadline >= filters["deadline_from"] + ) + if filters.get("deadline_to"): + query = query.filter( + GovernmentScheme.application_deadline <= filters["deadline_to"] + ) + + schemes = query.order_by(GovernmentScheme.application_deadline.asc()).all() + return [scheme.to_dict() for scheme in schemes] + + def get_scheme_by_id(self, scheme_id): + scheme = GovernmentScheme.query.get(scheme_id) + return scheme.to_dict() if scheme else None + + def check_eligibility(self, scheme_id, user_id, farm_id=None): + scheme = GovernmentScheme.query.get(scheme_id) + if not scheme: + return {"eligible": False, "reason": "Scheme not found"} + + user = User.query.get(user_id) + if not user: + return {"eligible": False, "reason": "User not found"} + + notes = [] + score = 0 + total_criteria = 0 + + total_criteria += 1 + if scheme.farm_size_min or scheme.farm_size_max: + if farm_id: + farm = Farm.query.get(farm_id) + if farm and farm.area_hectares: + area_acres = farm.area_hectares * 2.47105 + if scheme.farm_size_min and area_acres < scheme.farm_size_min: + notes.append( + f"Farm size {area_acres:.2f} acres is below minimum {scheme.farm_size_min} acres" + ) + elif scheme.farm_size_max and area_acres > scheme.farm_size_max: + notes.append( + f"Farm size {area_acres:.2f} acres exceeds maximum {scheme.farm_size_max} acres" + ) + else: + score += 1 + else: + notes.append("Farm size verification required") + else: + score += 1 + + total_criteria += 1 + if scheme.income_limit: + if hasattr(user, "annual_income") and user.annual_income: + if user.annual_income <= scheme.income_limit: + score += 1 + else: + notes.append(f"Income exceeds limit of {scheme.income_limit} INR") + else: + notes.append("Income verification required") + else: + score += 1 + + total_criteria += 1 + if scheme.region_specific and scheme.eligible_states: + if user.location: + eligible = False + states = json.loads(scheme.eligible_states) + for state in states: + if state.lower() in user.location.lower(): + eligible = True + break + if eligible: + score += 1 + else: + notes.append(f"Location {user.location} not in eligible states") + else: + notes.append("Location verification required") + else: + score += 1 + + eligibility_percentage = ( + (score / total_criteria) * 100 if total_criteria > 0 else 0 + ) + + return { + "eligible": eligibility_percentage >= 70, + "score": eligibility_percentage, + "notes": notes, + } + + def get_application_assistance(self, scheme_id): + scheme = GovernmentScheme.query.get(scheme_id) + if not scheme: + return None + + documents = ( + json.loads(scheme.required_documents) if scheme.required_documents else [] + ) + + assistance = { + "application_steps": [ + "Complete eligibility check", + "Gather required documents", + "Fill application form with accurate details", + "Upload supporting documents", + "Submit application before deadline", + "Track application status", + ], + "document_checklist": documents, + "timeline_suggestions": { + "recommended_submission": scheme.application_deadline + - timedelta(days=7) + if scheme.application_deadline + else None, + "last_submission": scheme.application_deadline, + }, + "contact_information": { + "ministry": scheme.ministry, + "scheme_code": scheme.scheme_code, + "application_portal": f"/schemes/{scheme_id}/apply", + }, + "tips": [ + "Double-check all document uploads are clear and readable", + "Ensure farm details match land records", + "Keep application number for tracking", + "Apply well before deadline to avoid last-minute issues", + "Contact scheme helpline if you face any issues", + ], + } + + return assistance + + def create_application( + self, scheme_id, user_id, farm_id=None, application_data=None + ): + scheme = GovernmentScheme.query.get(scheme_id) + if not scheme: + return {"success": False, "message": "Scheme not found"} + + eligibility = self.check_eligibility(scheme_id, user_id, farm_id) + + application = SchemeApplication( + scheme_id=scheme_id, + user_id=user_id, + farm_id=farm_id, + application_number=f"SCH{datetime.now().strftime('%Y%m%d%H%M%S')}", + status="draft", + eligibility_score=eligibility.get("score"), + eligibility_notes=json.dumps(eligibility.get("notes", [])), + farmer_details=json.dumps(application_data.get("farmer_details", {})) + if application_data + else "{}", + farm_details=json.dumps(application_data.get("farm_details", {})) + if application_data + else "{}", + submitted_documents=json.dumps(application_data.get("documents", [])) + if application_data + else "[]", + ) + + db.session.add(application) + db.session.commit() + + return { + "success": True, + "application_id": application.id, + "application_number": application.application_number, + "eligibility": eligibility, + } + + def submit_application(self, application_id): + application = SchemeApplication.query.get(application_id) + if not application: + return {"success": False, "message": "Application not found"} + + application.status = "submitted" + application.submitted_at = datetime.utcnow() + db.session.commit() + + return {"success": True, "message": "Application submitted successfully"} + + def get_user_applications(self, user_id, status=None): + query = SchemeApplication.query.filter_by(user_id=user_id) + + if status: + query = query.filter_by(status=status) + + applications = query.order_by(SchemeApplication.created_at.desc()).all() + return [app.to_dict() for app in applications] + + def create_deadline_reminders(self, scheme_id, user_id): + scheme = GovernmentScheme.query.get(scheme_id) + if not scheme or not scheme.application_deadline: + return [] + + reminders = [] + deadline = scheme.application_deadline + + reminder_dates = [ + (deadline - timedelta(days=30), "30 days before deadline"), + (deadline - timedelta(days=15), "15 days before deadline"), + (deadline - timedelta(days=7), "1 week before deadline"), + (deadline - timedelta(days=3), "3 days before deadline"), + (deadline - timedelta(days=1), "Last day reminder"), + ] + + for reminder_date, reminder_type in reminder_dates: + if datetime.now().date() <= reminder_date.date(): + reminder = SchemeReminder( + scheme_id=scheme_id, + user_id=user_id, + reminder_type="deadline_reminder", + reminder_date=datetime.combine(reminder_date, datetime.min.time()), + message=f"Reminder: {scheme.name} application deadline is on {deadline.strftime('%Y-%m-%d')}", + ) + db.session.add(reminder) + reminders.append(reminder) + + db.session.commit() + return reminders + + def get_upcoming_deadlines(self, days=30): + cutoff_date = datetime.now().date() + timedelta(days=days) + schemes = ( + GovernmentScheme.query.filter( + GovernmentScheme.is_active == True, + GovernmentScheme.application_deadline.isnot(None), + GovernmentScheme.application_deadline >= datetime.now().date(), + GovernmentScheme.application_deadline <= cutoff_date, + ) + .order_by(GovernmentScheme.application_deadline.asc()) + .all() + ) + + return [scheme.to_dict() for scheme in schemes] + + def search_schemes(self, keyword): + keyword = f"%{keyword}%" + schemes = GovernmentScheme.query.filter( + db.or_( + GovernmentScheme.name.ilike(keyword), + GovernmentScheme.description.ilike(keyword), + GovernmentScheme.ministry.ilike(keyword), + GovernmentScheme.scheme_code.ilike(keyword), + ), + GovernmentScheme.is_active == True, + ).all() + + return [scheme.to_dict() for scheme in schemes] + + +government_scheme_service = GovernmentSchemeService()