mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-18 21:44:08 +01:00
feat: Add farm_analytics table and update domain types
This commit is contained in:
parent
07e66c753d
commit
f4813e31b4
@ -6,84 +6,46 @@ import (
|
||||
)
|
||||
|
||||
type FarmAnalytics struct {
|
||||
FarmID string
|
||||
Name string
|
||||
OwnerID string
|
||||
LastUpdated time.Time
|
||||
WeatherData *WeatherAnalytics `json:"weather_data,omitempty"`
|
||||
InventoryData *InventoryAnalytics `json:"inventory_data,omitempty"`
|
||||
PlantHealthData *PlantHealthAnalytics `json:"plant_health_data,omitempty"`
|
||||
FinancialData *FinancialAnalytics `json:"financial_data,omitempty"`
|
||||
ProductionData *ProductionAnalytics `json:"production_data,omitempty"`
|
||||
FarmID string `json:"farm_id"`
|
||||
FarmName string `json:"farm_name"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
FarmType *string `json:"farm_type,omitempty"`
|
||||
TotalSize *string `json:"total_size,omitempty"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
Weather *WeatherData
|
||||
InventoryInfo struct {
|
||||
TotalItems int `json:"total_items"`
|
||||
LowStockCount int `json:"low_stock_count"`
|
||||
LastUpdated *time.Time `json:"last_updated,omitempty"`
|
||||
} `json:"inventory_info"`
|
||||
CropInfo struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
GrowingCount int `json:"growing_count"`
|
||||
LastUpdated *time.Time `json:"last_updated,omitempty"`
|
||||
} `json:"crop_info"`
|
||||
OverallStatus *string `json:"overall_status,omitempty"`
|
||||
AnalyticsLastUpdated time.Time `json:"analytics_last_updated"`
|
||||
}
|
||||
|
||||
type WeatherAnalytics struct {
|
||||
LastUpdated time.Time
|
||||
Temperature float64
|
||||
Humidity float64
|
||||
Rainfall float64
|
||||
WindSpeed float64
|
||||
WeatherStatus string
|
||||
AlertLevel string
|
||||
ForecastSummary string
|
||||
}
|
||||
|
||||
type InventoryAnalytics struct {
|
||||
LastUpdated time.Time
|
||||
TotalItems int
|
||||
LowStockItems int
|
||||
TotalValue float64
|
||||
RecentChanges []InventoryChange
|
||||
}
|
||||
|
||||
type InventoryChange struct {
|
||||
ItemID string
|
||||
ItemName string
|
||||
ChangeAmount float64
|
||||
ChangeType string
|
||||
ChangedAt time.Time
|
||||
}
|
||||
|
||||
type PlantHealthAnalytics struct {
|
||||
LastUpdated time.Time
|
||||
HealthyPlants int
|
||||
UnhealthyPlants int
|
||||
CriticalPlants int
|
||||
RecentHealthIssues []PlantHealthIssue
|
||||
}
|
||||
|
||||
type PlantHealthIssue struct {
|
||||
PlantID string
|
||||
PlantName string
|
||||
HealthStatus string
|
||||
AlertLevel string
|
||||
RecordedAt time.Time
|
||||
}
|
||||
|
||||
type FinancialAnalytics struct {
|
||||
LastUpdated time.Time
|
||||
TotalRevenue float64
|
||||
TotalExpenses float64
|
||||
NetProfit float64
|
||||
RecentTransactions []TransactionSummary
|
||||
}
|
||||
|
||||
type TransactionSummary struct {
|
||||
TransactionID string
|
||||
Type string
|
||||
Amount float64
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ProductionAnalytics struct {
|
||||
LastUpdated time.Time
|
||||
TotalProduction float64
|
||||
YieldRate float64
|
||||
HarvestForecast float64
|
||||
type CropAnalytics struct {
|
||||
CropID string `json:"crop_id"`
|
||||
CropName string `json:"crop_name"`
|
||||
FarmID string `json:"farm_id"`
|
||||
PlantName string `json:"plant_name"`
|
||||
Variety *string `json:"variety,omitempty"`
|
||||
CurrentStatus string `json:"current_status"`
|
||||
GrowthStage string `json:"growth_stage"`
|
||||
LandSize float64 `json:"land_size"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
type AnalyticsRepository interface {
|
||||
GetFarmAnalytics(ctx context.Context, farmID string) (*FarmAnalytics, error)
|
||||
SaveFarmAnalytics(ctx context.Context, farmID string, data interface{}) error
|
||||
CreateOrUpdateFarmBaseData(ctx context.Context, farm *Farm) error
|
||||
UpdateFarmAnalyticsWeather(ctx context.Context, farmID string, weatherData *WeatherData) error
|
||||
UpdateFarmAnalyticsCropStats(ctx context.Context, farmID string) error
|
||||
UpdateFarmAnalyticsInventoryStats(ctx context.Context, farmID string) error
|
||||
DeleteFarmAnalytics(ctx context.Context, farmID string) error
|
||||
UpdateFarmOverallStatus(ctx context.Context, farmID string, status string) error
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
@ -16,6 +17,7 @@ type Cropland struct {
|
||||
GrowthStage string
|
||||
PlantID string
|
||||
FarmID string
|
||||
GeoFeature json.RawMessage
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@ -32,7 +34,8 @@ func (c *Cropland) Validate() error {
|
||||
type CroplandRepository interface {
|
||||
GetByID(context.Context, string) (Cropland, error)
|
||||
GetByFarmID(ctx context.Context, farmID string) ([]Cropland, error)
|
||||
GetAll(ctx context.Context) ([]Cropland, error) // Add this method
|
||||
GetAll(ctx context.Context) ([]Cropland, error)
|
||||
CreateOrUpdate(context.Context, *Cropland) error
|
||||
Delete(context.Context, string) error
|
||||
SetEventPublisher(EventPublisher)
|
||||
}
|
||||
|
||||
@ -8,16 +8,16 @@ import (
|
||||
)
|
||||
|
||||
type Farm struct {
|
||||
UUID string
|
||||
Name string
|
||||
Lat float64 // single latitude value
|
||||
Lon float64 // single longitude value
|
||||
FarmType string // e.g., "Durian", "mango", "mixed-crop", "others"
|
||||
TotalSize string // e.g., "10 Rai" (optional)
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
OwnerID string
|
||||
Crops []Cropland
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Lat float64 `json:"latitude"`
|
||||
Lon float64 `json:"longitude"`
|
||||
FarmType string `json:"farm_type,omitempty"`
|
||||
TotalSize string `json:"total_size,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
Crops []Cropland `json:"crops,omitempty"`
|
||||
}
|
||||
|
||||
func (f *Farm) Validate() error {
|
||||
|
||||
21
backend/internal/domain/weather.go
Normal file
21
backend/internal/domain/weather.go
Normal file
@ -0,0 +1,21 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WeatherData struct {
|
||||
TempCelsius *float64 `json:"temp_celsius,omitempty"`
|
||||
Humidity *float64 `json:"humidity,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
WindSpeed *float64 `json:"wind_speed,omitempty"`
|
||||
RainVolume1h *float64 `json:"rain_volume_1h,omitempty"`
|
||||
ObservedAt *time.Time `json:"observed_at,omitempty"`
|
||||
WeatherLastUpdated *time.Time `json:"weather_last_updated,omitempty"`
|
||||
}
|
||||
|
||||
type WeatherFetcher interface {
|
||||
GetCurrentWeatherByCoords(ctx context.Context, lat, lon float64) (*WeatherData, error)
|
||||
}
|
||||
159
backend/migrations/00014_create_farm_analytics_table.sql
Normal file
159
backend/migrations/00014_create_farm_analytics_table.sql
Normal file
@ -0,0 +1,159 @@
|
||||
-- +goose Up
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS public.farm_analytics_view CASCADE;
|
||||
DROP FUNCTION IF EXISTS public.refresh_farm_analytics_view() CASCADE;
|
||||
DROP MATERIALIZED VIEW IF EXISTS public.crop_analytics_view CASCADE;
|
||||
DROP FUNCTION IF EXISTS public.refresh_crop_analytics_view() CASCADE;
|
||||
|
||||
CREATE TABLE public.farm_analytics (
|
||||
farm_id UUID PRIMARY KEY NOT NULL,
|
||||
farm_name TEXT NOT NULL,
|
||||
owner_id UUID NOT NULL,
|
||||
farm_type TEXT,
|
||||
total_size TEXT,
|
||||
latitude DOUBLE PRECISION NOT NULL,
|
||||
longitude DOUBLE PRECISION NOT NULL,
|
||||
|
||||
weather_temp_celsius DOUBLE PRECISION,
|
||||
weather_humidity DOUBLE PRECISION,
|
||||
weather_description TEXT,
|
||||
weather_icon TEXT,
|
||||
weather_wind_speed DOUBLE PRECISION,
|
||||
weather_rain_1h DOUBLE PRECISION,
|
||||
weather_observed_at TIMESTAMPTZ, -- Timestamp from the weather data itself
|
||||
weather_last_updated TIMESTAMPTZ, -- Timestamp when weather was last fetched/updated in this record
|
||||
|
||||
inventory_total_items INT DEFAULT 0 NOT NULL,
|
||||
inventory_low_stock_count INT DEFAULT 0 NOT NULL,
|
||||
inventory_last_updated TIMESTAMPTZ,
|
||||
|
||||
crop_total_count INT DEFAULT 0 NOT NULL,
|
||||
crop_growing_count INT DEFAULT 0 NOT NULL, -- Example: specific status count
|
||||
crop_last_updated TIMESTAMPTZ,
|
||||
|
||||
overall_status TEXT, -- e.g., 'ok', 'warning', 'critical' - Can be updated by various events
|
||||
|
||||
analytics_last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When this specific analytics record was last touched
|
||||
|
||||
CONSTRAINT fk_farm_analytics_farm FOREIGN KEY (farm_id) REFERENCES public.farms(uuid) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_farm_analytics_owner FOREIGN KEY (owner_id) REFERENCES public.users(uuid) ON DELETE CASCADE -- Assuming owner_id refers to users.uuid
|
||||
);
|
||||
|
||||
CREATE INDEX idx_farm_analytics_owner_id ON public.farm_analytics(owner_id);
|
||||
CREATE INDEX idx_farm_analytics_last_updated ON public.farm_analytics(analytics_last_updated DESC);
|
||||
CREATE INDEX idx_farm_analytics_weather_last_updated ON public.farm_analytics(weather_last_updated DESC);
|
||||
|
||||
-- Optional: Initial data population (run once after table creation if needed)
|
||||
-- INSERT INTO public.farm_analytics (farm_id, farm_name, owner_id, farm_type, total_size, latitude, longitude, analytics_last_updated)
|
||||
-- SELECT uuid, name, owner_id, farm_type, total_size, lat, lon, updated_at
|
||||
-- FROM public.farms
|
||||
-- ON CONFLICT (farm_id) DO NOTHING;
|
||||
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DROP TABLE IF EXISTS public.farm_analytics;
|
||||
|
||||
CREATE MATERIALIZED VIEW public.farm_analytics_view AS
|
||||
SELECT
|
||||
f.uuid AS farm_id,
|
||||
f.name AS farm_name,
|
||||
f.owner_id,
|
||||
f.farm_type,
|
||||
f.total_size,
|
||||
COALESCE(
|
||||
(SELECT MAX(ae_max.created_at) FROM public.analytics_events ae_max WHERE ae_max.farm_id = f.uuid),
|
||||
f.updated_at
|
||||
) AS last_updated,
|
||||
(
|
||||
SELECT jsonb_build_object(
|
||||
'last_updated', latest_weather.created_at,
|
||||
'temperature', (latest_weather.event_data->>'temperature')::float,
|
||||
'humidity', (latest_weather.event_data->>'humidity')::float,
|
||||
'rainfall', (latest_weather.event_data->>'rainfall')::float,
|
||||
'wind_speed', (latest_weather.event_data->>'wind_speed')::float,
|
||||
'weather_status', latest_weather.event_data->>'weather_status',
|
||||
'alert_level', latest_weather.event_data->>'alert_level',
|
||||
'forecast_summary', latest_weather.event_data->>'forecast_summary'
|
||||
)
|
||||
FROM (
|
||||
SELECT ae_w.event_data, ae_w.created_at
|
||||
FROM public.analytics_events ae_w
|
||||
WHERE ae_w.farm_id = f.uuid AND ae_w.event_type = 'weather.updated'
|
||||
ORDER BY ae_w.created_at DESC
|
||||
LIMIT 1
|
||||
) AS latest_weather
|
||||
) AS weather_data,
|
||||
(
|
||||
SELECT jsonb_build_object(
|
||||
'items', COALESCE(jsonb_agg(ae_i.event_data->'items' ORDER BY (ae_i.event_data->>'timestamp') DESC) FILTER (WHERE ae_i.event_data ? 'items'), '[]'::jsonb),
|
||||
'last_updated', MAX(ae_i.created_at)
|
||||
)
|
||||
FROM analytics_events ae_i
|
||||
WHERE ae_i.farm_id = f.uuid AND ae_i.event_type = 'inventory.updated'
|
||||
) AS inventory_data,
|
||||
(
|
||||
SELECT jsonb_build_object(
|
||||
'status', MAX(ae_p.event_data->>'status'),
|
||||
'issues', COALESCE(jsonb_agg(ae_p.event_data->'issues') FILTER (WHERE ae_p.event_data ? 'issues'), '[]'::jsonb),
|
||||
'last_updated', MAX(ae_p.created_at)
|
||||
)
|
||||
FROM analytics_events ae_p
|
||||
WHERE ae_p.farm_id = f.uuid AND ae_p.event_type = 'plant_health.updated'
|
||||
) AS plant_health_data,
|
||||
(
|
||||
SELECT jsonb_build_object(
|
||||
'yield_total', SUM((ae_pr.event_data->>'yield')::float) FILTER (WHERE ae_pr.event_data ? 'yield'),
|
||||
'forecast_latest', MAX(ae_pr.event_data->>'forecast') FILTER (WHERE ae_pr.event_data ? 'forecast'),
|
||||
'last_updated', MAX(ae_pr.created_at)
|
||||
)
|
||||
FROM analytics_events ae_pr
|
||||
WHERE ae_pr.farm_id = f.uuid AND ae_pr.event_type = 'production.updated'
|
||||
) AS production_data
|
||||
FROM
|
||||
public.farms f;
|
||||
|
||||
CREATE UNIQUE INDEX idx_farm_analytics_view_farm_id ON public.farm_analytics_view(farm_id);
|
||||
CREATE INDEX idx_farm_analytics_view_owner_id ON public.farm_analytics_view(owner_id);
|
||||
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION public.refresh_farm_analytics_view()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY public.farm_analytics_view;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
CREATE TRIGGER refresh_farm_analytics_view_trigger_events
|
||||
AFTER INSERT ON public.analytics_events -- Adjust if original trigger was different
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION public.refresh_farm_analytics_view();
|
||||
|
||||
CREATE TRIGGER refresh_farm_analytics_view_trigger_farms
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.farms -- Adjust if original trigger was different
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION public.refresh_farm_analytics_view();
|
||||
|
||||
CREATE MATERIALIZED VIEW public.crop_analytics_view AS
|
||||
SELECT
|
||||
c.uuid AS crop_id, c.name AS crop_name, c.farm_id, p.name AS plant_name, p.variety AS variety,
|
||||
c.status AS current_status, c.growth_stage, c.land_size, c.geo_feature, c.updated_at AS last_updated
|
||||
FROM public.croplands c JOIN public.plants p ON c.plant_id = p.uuid;
|
||||
CREATE UNIQUE INDEX idx_crop_analytics_view_crop_id ON public.crop_analytics_view(crop_id);
|
||||
CREATE INDEX idx_crop_analytics_view_farm_id ON public.crop_analytics_view(farm_id);
|
||||
CREATE INDEX idx_crop_analytics_view_plant_name ON public.crop_analytics_view(plant_name);
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION public.refresh_crop_analytics_view()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY public.crop_analytics_view;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
CREATE TRIGGER refresh_crop_analytics_trigger_croplands
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.croplands FOR EACH STATEMENT EXECUTE FUNCTION public.refresh_crop_analytics_view();
|
||||
CREATE TRIGGER refresh_crop_analytics_trigger_plants
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.plants FOR EACH STATEMENT EXECUTE FUNCTION public.refresh_crop_analytics_view();
|
||||
Loading…
Reference in New Issue
Block a user