Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions JobFlow.API/Controllers/OnboardingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ public async Task<IResult> RecordEvent([FromBody] OnboardingAnalyticsEventReques
: result.ToProblemDetails();
}

[HttpGet("defaults")]
public async Task<IResult> GetIndustryDefaults()
{
var organizationId = HttpContext.GetOrganizationId();
var result = await onboarding.GetIndustryDefaultsAsync(organizationId);
return result.IsSuccess
? Results.Ok(result.Value)
: result.ToProblemDetails();
}

[HttpPost("seed-defaults")]
public async Task<IResult> SeedIndustryDefaults()
{
var organizationId = HttpContext.GetOrganizationId();
var result = await onboarding.SeedIndustryDefaultsAsync(organizationId);
return result.IsSuccess
? Results.Ok()
: result.ToProblemDetails();
}

private static bool HasMinPlan(string? planName, string required)
{
static int Rank(string? plan)
Expand Down
17 changes: 17 additions & 0 deletions JobFlow.Business/Models/DTOs/OnboardingIndustryDefaultsDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace JobFlow.Business.Models.DTOs;

public class OnboardingIndustryDefaultsDto
{
public string OrgTypeName { get; set; } = string.Empty;
public string TemplateSuggestionName { get; set; } = string.Empty;
public int PaymentTermsDays { get; set; }
public IReadOnlyList<OnboardingIndustryServiceDto> Services { get; set; } = [];
}

public class OnboardingIndustryServiceDto
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Unit { get; set; } = string.Empty;
public decimal Price { get; set; }
}
98 changes: 98 additions & 0 deletions JobFlow.Business/Onboarding/OnboardingIndustryDefaultsCatalog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
namespace JobFlow.Business.Onboarding;

public record IndustryServiceSeed(
string Name,
string Description,
string Unit,
decimal Price);

public record IndustryDefaults(
string OrgTypeName,
string TemplateSuggestionName,
int PaymentTermsDays,
IReadOnlyList<IndustryServiceSeed> Services);

public static class OnboardingIndustryDefaultsCatalog
{
public static readonly IReadOnlyList<IndustryDefaults> Catalog =
[
new IndustryDefaults(
"Landscaping",
"Landscaping Estimate",
15,
[
new IndustryServiceSeed("Lawn mowing", "Standard residential lawn cut.", "cut", 85m),
new IndustryServiceSeed("Hedge trimming", "Trim and shape shrubs and hedges.", "visit", 120m),
new IndustryServiceSeed("Leaf cleanup", "Seasonal leaf removal and disposal.", "visit", 150m),
new IndustryServiceSeed("Irrigation check", "Inspection and adjustment of irrigation system.", "visit", 95m),
new IndustryServiceSeed("Mulch install", "Supply and spread mulch.", "yard", 65m),
new IndustryServiceSeed("Landscape design consult", "On-site design consultation.", "hour", 125m)
]
),
new IndustryDefaults(
"Cleaning",
"Cleaning Service Estimate",
0,
[
new IndustryServiceSeed("Standard house cleaning", "Routine cleaning of all rooms.", "visit", 150m),
new IndustryServiceSeed("Deep clean", "Thorough top-to-bottom cleaning.", "visit", 250m),
new IndustryServiceSeed("Move-in/move-out clean", "Full property clean for move-in or move-out.", "job", 350m),
new IndustryServiceSeed("Office cleaning", "Commercial office space cleaning.", "visit", 200m),
new IndustryServiceSeed("Window cleaning", "Interior and exterior window washing.", "visit", 120m),
new IndustryServiceSeed("Carpet cleaning", "Steam or dry carpet cleaning per room.", "room", 75m)
]
),
new IndustryDefaults(
"IT",
"IT Services Estimate",
30,
[
new IndustryServiceSeed("Remote support", "Remote troubleshooting and repair.", "hour", 120m),
new IndustryServiceSeed("On-site visit", "Technician on-site support.", "visit", 175m),
new IndustryServiceSeed("Network setup", "Design and install LAN/WiFi infrastructure.", "job", 450m),
new IndustryServiceSeed("Workstation setup", "Configure and provision a workstation.", "unit", 250m),
new IndustryServiceSeed("Managed services", "Monthly monitoring and maintenance plan.", "month", 299m),
new IndustryServiceSeed("Data backup setup", "Configure cloud or local backup solution.", "job", 350m)
]
),
new IndustryDefaults(
"HVAC",
"HVAC Estimate",
15,
[
new IndustryServiceSeed("Diagnostic visit", "On-site diagnosis and troubleshooting.", "visit", 95m),
new IndustryServiceSeed("AC tune-up", "Seasonal air conditioning service.", "unit", 149m),
new IndustryServiceSeed("Furnace inspection", "Annual furnace safety inspection.", "unit", 129m),
new IndustryServiceSeed("Filter replacement", "Supply and install replacement filter.", "unit", 45m),
new IndustryServiceSeed("Refrigerant recharge", "Recharge AC refrigerant to spec.", "unit", 250m),
new IndustryServiceSeed("System installation", "Full HVAC system install.", "job", 2500m)
]
),
new IndustryDefaults(
"General",
"General Services Estimate",
30,
[
new IndustryServiceSeed("Service call", "Initial on-site service visit.", "visit", 89m),
new IndustryServiceSeed("Labor", "General field labor.", "hour", 95m),
new IndustryServiceSeed("Materials", "Estimated materials and supplies.", "lot", 250m),
new IndustryServiceSeed("Project consultation", "Planning and scoping consultation.", "hour", 75m),
new IndustryServiceSeed("Standard repair", "Most common repair or fix.", "job", 200m)
]
)
];

public static IndustryDefaults? TryGetByOrgTypeName(string? orgTypeName)
{
if (string.IsNullOrWhiteSpace(orgTypeName))
return null;

return Catalog.FirstOrDefault(c =>
c.OrgTypeName.Equals(orgTypeName.Trim(), StringComparison.OrdinalIgnoreCase));
}

public static IndustryDefaults GetGeneralDefaults()
{
return Catalog.First(c => c.OrgTypeName == "General");
}
}
91 changes: 91 additions & 0 deletions JobFlow.Business/Services/OnboardingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class OnboardingService : IOnboardingService
private readonly IRepository<Organization> orgRepo;
private readonly IRepository<PriceBookItem> priceBookItems;
private readonly IRepository<OrganizationOnboardingStep> stepRepo;
private readonly IRepository<OrganizationInvoicingSettings> invoicingSettingsRepo;
private readonly IWorkflowSettingsService workflowSettings;
private readonly IUnitOfWork uow;

Expand All @@ -26,6 +27,7 @@ public OnboardingService(IUnitOfWork uow, IWorkflowSettingsService workflowSetti
orgRepo = uow.RepositoryOf<Organization>();
stepRepo = uow.RepositoryOf<OrganizationOnboardingStep>();
priceBookItems = uow.RepositoryOf<PriceBookItem>();
invoicingSettingsRepo = uow.RepositoryOf<OrganizationInvoicingSettings>();
}

public async Task<Result<IEnumerable<OnboardingStepDto>>> GetChecklistAsync(Guid orgId)
Expand Down Expand Up @@ -274,4 +276,93 @@ await eventsRepo.AddAsync(new OrganizationOnboardingEvent
await uow.SaveChangesAsync();
return Result.Success();
}

public async Task<Result<OnboardingIndustryDefaultsDto>> GetIndustryDefaultsAsync(Guid organizationId)
{
var org = await orgRepo.Query()
.Include(o => o.OrganizationType)
.FirstOrDefaultAsync(o => o.Id == organizationId);

if (org == null)
return Result.Failure<OnboardingIndustryDefaultsDto>(OnboardingErrors.OrganizationNotFound);

var defaults = OnboardingIndustryDefaultsCatalog.TryGetByOrgTypeName(org.OrganizationType?.TypeName)
?? OnboardingIndustryDefaultsCatalog.GetGeneralDefaults();

var dto = new OnboardingIndustryDefaultsDto
{
OrgTypeName = defaults.OrgTypeName,
TemplateSuggestionName = defaults.TemplateSuggestionName,
PaymentTermsDays = defaults.PaymentTermsDays,
Services = defaults.Services
.Select(s => new OnboardingIndustryServiceDto
{
Name = s.Name,
Description = s.Description,
Unit = s.Unit,
Price = s.Price
})
.ToList()
};

return Result.Success(dto);
}

public async Task<Result> SeedIndustryDefaultsAsync(Guid organizationId)
{
var org = await orgRepo.Query()
.Include(o => o.OrganizationType)
.FirstOrDefaultAsync(o => o.Id == organizationId);

if (org == null)
return Result.Failure(OnboardingErrors.OrganizationNotFound);

var defaults = OnboardingIndustryDefaultsCatalog.TryGetByOrgTypeName(org.OrganizationType?.TypeName)
?? OnboardingIndustryDefaultsCatalog.GetGeneralDefaults();

var existingNames = await priceBookItems.Query()
.Where(x => x.OrganizationId == organizationId)
.Select(x => x.Name)
.ToListAsync();

foreach (var service in defaults.Services)
{
if (existingNames.Any(name =>
string.Equals(name, service.Name, StringComparison.OrdinalIgnoreCase)))
continue;

await priceBookItems.AddAsync(new PriceBookItem
{
OrganizationId = organizationId,
Name = service.Name,
Description = service.Description,
Unit = service.Unit,
Price = service.Price,
Cost = 0m,
PricePerUnit = service.Price,
ItemType = PriceBookItemType.Service
});
}

// Apply industry payment terms to invoicing settings
var invoicingSettings = await invoicingSettingsRepo.Query()
.FirstOrDefaultAsync(x => x.OrganizationId == organizationId);

if (invoicingSettings == null)
{
await invoicingSettingsRepo.AddAsync(new OrganizationInvoicingSettings
{
OrganizationId = organizationId,
PaymentTermsDays = defaults.PaymentTermsDays
});
}
else
{
invoicingSettings.PaymentTermsDays = defaults.PaymentTermsDays;
invoicingSettingsRepo.Update(invoicingSettings);
}

await uow.SaveChangesAsync();
return Result.Success();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ Task<Result<OnboardingQuickStartStateDto>> ApplyQuickStartAsync(
Guid organizationId,
OnboardingQuickStartApplyRequestDto request);
Task<Result> RecordAnalyticsEventAsync(Guid organizationId, string stepName, string eventType);
Task<Result<OnboardingIndustryDefaultsDto>> GetIndustryDefaultsAsync(Guid organizationId);
Task<Result> SeedIndustryDefaultsAsync(Guid organizationId);
}
3 changes: 3 additions & 0 deletions JobFlow.Domain/Models/OrganizationInvoicingSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ public class OrganizationInvoicingSettings : Entity

public bool DepositRequired { get; set; }
public decimal DepositPercentage { get; set; }

/// <summary>Net payment terms in days. 0 = due on receipt.</summary>
public int PaymentTermsDays { get; set; } = 0;
}
Loading
Loading