Initial commit
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps Account.CurrentBalance in sync with double-entry transactions.
|
||||
/// DebitAsync / CreditAsync update the balance in the normal-balance direction
|
||||
/// for each account sub-type, but do NOT call CompleteAsync — the caller must
|
||||
/// persist by calling IUnitOfWork.CompleteAsync / CommitTransactionAsync.
|
||||
/// RecalculateAllAsync is the exception: it saves internally and is safe to call standalone.
|
||||
/// </summary>
|
||||
public interface IAccountBalanceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a debit to the account.
|
||||
/// Debit-normal accounts (Asset / Expense / COGS): balance increases.
|
||||
/// Credit-normal accounts (Liability / Equity / Revenue): balance decreases.
|
||||
/// No-op when accountId is null or amount is zero.
|
||||
/// </summary>
|
||||
Task DebitAsync(int? accountId, decimal amount);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a credit to the account.
|
||||
/// Debit-normal accounts: balance decreases.
|
||||
/// Credit-normal accounts: balance increases.
|
||||
/// No-op when accountId is null or amount is zero.
|
||||
/// </summary>
|
||||
Task CreditAsync(int? accountId, decimal amount);
|
||||
|
||||
/// <summary>
|
||||
/// Recomputes CurrentBalance for every active account in the company by replaying all
|
||||
/// transactions through LedgerService. Saves internally. Use after import or to fix drift.
|
||||
/// </summary>
|
||||
Task RecalculateAllAsync(int companyId);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using PowderCoating.Application.DTOs.AI;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IAccountingAiService
|
||||
{
|
||||
/// <summary>
|
||||
/// Scans a receipt/invoice image and extracts vendor, date, total, invoice number, and line items.
|
||||
/// Attempts to match each line item to one of the provided expense accounts.
|
||||
/// </summary>
|
||||
Task<ReceiptScanResult> ScanReceiptAsync(
|
||||
byte[] imageData,
|
||||
string mimeType,
|
||||
List<AccountSummary> availableAccounts);
|
||||
|
||||
/// <summary>
|
||||
/// Drafts a follow-up email for an overdue AR customer.
|
||||
/// Tone scales with days overdue: gentle (≤30), firm (31-60), serious (61+).
|
||||
/// </summary>
|
||||
Task<ArFollowUpResult> DraftFollowUpEmailAsync(ArFollowUpRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Suggests the best-matching expense account for a bill line item or expense.
|
||||
/// Returns a primary suggestion plus up to 3 ranked alternatives.
|
||||
/// </summary>
|
||||
Task<AccountSuggestionResult> SuggestAccountAsync(AccountSuggestionRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a plain-English financial health summary with 4-6 bullet points
|
||||
/// and a sentiment classification (positive / neutral / concerning).
|
||||
/// </summary>
|
||||
Task<FinancialSummaryResult> GenerateFinancialSummaryAsync(FinancialSummaryRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Projects 30/60/90-day cash position based on open AR, open AP, and active job pipeline.
|
||||
/// Returns period-by-period inflow/outflow estimates and plain-English insights.
|
||||
/// </summary>
|
||||
Task<CashFlowForecastResult> GenerateCashFlowForecastAsync(CashFlowForecastRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Scans recent bills and expense account trends for duplicates, unusual amounts,
|
||||
/// and accounts running significantly above historical averages.
|
||||
/// Returns a ranked list of flagged items with recommended actions.
|
||||
/// </summary>
|
||||
Task<AnomalyDetectionResult> DetectAnomaliesAsync(AnomalyDetectionRequest request);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IAdminNotificationService
|
||||
{
|
||||
Task NotifyNewCompanyRegisteredAsync(int companyId, string companyName, string planName, string contactName, string contactEmail);
|
||||
Task NotifyBugReportSubmittedAsync(int bugReportId, string title, string description, string priority, string submittedByName, string companyName);
|
||||
Task NotifyCompanyExpiredAsync(int companyId, string companyName, string contactEmail, DateTime expiredOn);
|
||||
Task NotifyCompanyGracePeriodAsync(int companyId, string companyName, string contactEmail, DateTime gracePeriodEndsOn);
|
||||
Task NotifyContactFormSubmittedAsync(string senderName, string senderEmail, string companyName, string category, string subject, string message);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IAiHelpService
|
||||
{
|
||||
/// <summary>
|
||||
/// Send a message to the AI help assistant and get a response.
|
||||
/// </summary>
|
||||
/// <param name="conversationHistory">Prior turns: alternating user/assistant messages.</param>
|
||||
/// <param name="userMessage">The current user message.</param>
|
||||
/// <param name="tenantContext">Read-only context about the current user and company.</param>
|
||||
Task<string> SendMessageAsync(
|
||||
List<AiHelpMessage> conversationHistory,
|
||||
string userMessage,
|
||||
string systemPrompt);
|
||||
}
|
||||
|
||||
public record AiHelpMessage(string Role, string Content);
|
||||
|
||||
public record AiHelpTenantContext(
|
||||
string CompanyName,
|
||||
string UserRole,
|
||||
string UserName,
|
||||
string? SubscriptionPlan);
|
||||
@@ -0,0 +1,18 @@
|
||||
using PowderCoating.Application.DTOs.AI;
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IAiQuoteService
|
||||
{
|
||||
/// <summary>
|
||||
/// Analyze item photo(s) and return an estimated quote or a follow-up question.
|
||||
/// </summary>
|
||||
Task<AiAnalyzeItemResult> AnalyzeItemAsync(
|
||||
AiAnalyzeItemRequest request,
|
||||
List<(byte[] Data, string ContentType, string FileName)> photos,
|
||||
CompanyOperatingCosts costs,
|
||||
decimal avgPowderCostPerLb,
|
||||
CompanyAiContext? context = null,
|
||||
CompanyBlastSetup? selectedBlastSetup = null);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using PowderCoating.Application.DTOs.Scheduling;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IAiSchedulingService
|
||||
{
|
||||
Task<BatchScheduleSuggestion> SuggestBatchesAsync(BatchSchedulingRequest request);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Logs Anthropic API calls to the AiUsageLog table for platform-wide cost visibility.
|
||||
/// Implementations must be fault-tolerant — a logging failure must never break the caller.
|
||||
/// </summary>
|
||||
public interface IAiUsageLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Records a single AI API call. Fire-and-forget safe — swallows all exceptions internally.
|
||||
/// </summary>
|
||||
/// <param name="companyId">Tenant that triggered the call.</param>
|
||||
/// <param name="userId">Identity user ID of the authenticated user.</param>
|
||||
/// <param name="feature">One of the AppConstants.AiFeatures constants.</param>
|
||||
/// <param name="success">False when the AI service threw or returned an error.</param>
|
||||
/// <param name="inputLength">Character or byte count of the input — rough token-cost proxy.</param>
|
||||
Task LogAsync(int companyId, string userId, string feature, bool success = true, int inputLength = 0);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Writes manual entries to the AuditLog table for operations that are not captured
|
||||
/// automatically by the EF SaveChanges interceptor — imports, exports, and future
|
||||
/// platform-level events. Uses the same table and viewer as the interceptor so all
|
||||
/// audit history is in one place.
|
||||
/// </summary>
|
||||
public interface IAuditService
|
||||
{
|
||||
/// <summary>
|
||||
/// Appends a single row to AuditLogs outside of the EF change tracker.
|
||||
/// </summary>
|
||||
/// <param name="action">Verb describing the event, e.g. "Imported", "Exported".</param>
|
||||
/// <param name="entityType">Logical category, e.g. "Customers", "AccountingExport".</param>
|
||||
/// <param name="description">Short human-readable label shown in the audit viewer.</param>
|
||||
/// <param name="details">Optional object serialised to JSON and stored in NewValues.</param>
|
||||
/// <param name="entityId">Optional identifier for the affected record.</param>
|
||||
Task LogAsync(string action, string entityType, string? description = null,
|
||||
object? details = null, string? entityId = null);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IAzureBlobStorageService
|
||||
{
|
||||
Task<(bool Success, string ErrorMessage)> UploadAsync(
|
||||
string containerName,
|
||||
string blobName,
|
||||
Stream content,
|
||||
string contentType);
|
||||
|
||||
Task<(bool Success, byte[] Content, string ContentType, string ErrorMessage)> DownloadAsync(
|
||||
string containerName,
|
||||
string blobName);
|
||||
|
||||
Task<(bool Success, string ErrorMessage)> DeleteAsync(
|
||||
string containerName,
|
||||
string blobName);
|
||||
|
||||
Task<bool> ExistsAsync(string containerName, string blobName);
|
||||
|
||||
Task<IEnumerable<string>> ListBlobsByPrefixAsync(string containerName, string prefix);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using PowderCoating.Application.DTOs.Health;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a tenant company's operational configuration is complete.
|
||||
/// Separate from churn-risk health (login/engagement signals) — this catches
|
||||
/// setup gaps that break features before the customer files a support ticket.
|
||||
/// </summary>
|
||||
public interface ICompanyConfigHealthService
|
||||
{
|
||||
/// <summary>Checks one company and returns its config issues.</summary>
|
||||
Task<CompanyConfigHealth> CheckAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Batch-checks multiple companies in a fixed number of DB round-trips
|
||||
/// (one query per check type, not one query per company).
|
||||
/// </summary>
|
||||
Task<Dictionary<int, CompanyConfigHealth>> CheckBatchAsync(IEnumerable<int> companyIds);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface ICompanyLogoService
|
||||
{
|
||||
/// <summary>
|
||||
/// Save company logo to filesystem
|
||||
/// </summary>
|
||||
Task<(bool Success, string FilePath, string ErrorMessage)> SaveCompanyLogoAsync(IFormFile file, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Delete company logo from filesystem
|
||||
/// </summary>
|
||||
Task<(bool Success, string ErrorMessage)> DeleteCompanyLogoAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Get company logo from filesystem
|
||||
/// </summary>
|
||||
Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetCompanyLogoAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Check if company logo exists
|
||||
/// </summary>
|
||||
Task<bool> CompanyLogoExistsAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Get the expected logo path for a company
|
||||
/// </summary>
|
||||
string GetCompanyLogoPath(int companyId, string extension);
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using PowderCoating.Application.DTOs.Import;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for bulk importing data via CSV files.
|
||||
/// Supports customers, catalog items, and inventory items.
|
||||
/// </summary>
|
||||
public interface ICsvImportService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for customer imports.
|
||||
/// </summary>
|
||||
/// <returns>CSV file content as byte array</returns>
|
||||
byte[] GenerateCustomerTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for catalog item imports.
|
||||
/// </summary>
|
||||
/// <returns>CSV file content as byte array</returns>
|
||||
byte[] GenerateCatalogItemTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for inventory item imports.
|
||||
/// </summary>
|
||||
/// <returns>CSV file content as byte array</returns>
|
||||
byte[] GenerateInventoryItemTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import customers from a CSV stream.
|
||||
/// </summary>
|
||||
/// <param name="csvStream">CSV file stream</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <returns>Import result with success/error counts</returns>
|
||||
Task<CsvImportResultDto> ImportCustomersAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Import catalog items from a CSV stream.
|
||||
/// Creates categories on-the-fly if they don't exist.
|
||||
/// </summary>
|
||||
/// <param name="csvStream">CSV file stream</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="revenueAccountId">Optional revenue account to assign to all imported items</param>
|
||||
/// <param name="cogsAccountId">Optional COGS account to assign to all imported items</param>
|
||||
/// <returns>Import result with success/error counts</returns>
|
||||
Task<CsvImportResultDto> ImportCatalogItemsAsync(Stream csvStream, int companyId, int? revenueAccountId = null, int? cogsAccountId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Import inventory items from a CSV stream.
|
||||
/// </summary>
|
||||
/// <param name="csvStream">CSV file stream</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="inventoryAccountId">Optional inventory asset account to assign to all imported items</param>
|
||||
/// <param name="cogsAccountId">Optional COGS account to assign to all imported items</param>
|
||||
/// <returns>Import result with success/error counts</returns>
|
||||
Task<CsvImportResultDto> ImportInventoryItemsAsync(Stream csvStream, int companyId, int? inventoryAccountId = null, int? cogsAccountId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for quote imports.
|
||||
/// </summary>
|
||||
/// <returns>CSV file content as byte array</returns>
|
||||
byte[] GenerateQuoteTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import quotes from a CSV stream.
|
||||
/// </summary>
|
||||
/// <param name="csvStream">CSV file stream</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <returns>Import result with success/error counts</returns>
|
||||
Task<CsvImportResultDto> ImportQuotesAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for job imports.
|
||||
/// </summary>
|
||||
/// <returns>CSV file content as byte array</returns>
|
||||
byte[] GenerateJobTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import jobs from a CSV stream.
|
||||
/// </summary>
|
||||
/// <param name="csvStream">CSV file stream</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <returns>Import result with success/error counts</returns>
|
||||
Task<CsvImportResultDto> ImportJobsAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for appointment imports.
|
||||
/// </summary>
|
||||
/// <returns>CSV file content as byte array</returns>
|
||||
byte[] GenerateAppointmentTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import appointments from a CSV stream.
|
||||
/// </summary>
|
||||
/// <param name="csvStream">CSV file stream</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <returns>Import result with success/error counts</returns>
|
||||
Task<CsvImportResultDto> ImportAppointmentsAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for equipment imports.
|
||||
/// </summary>
|
||||
/// <returns>CSV file content as byte array</returns>
|
||||
byte[] GenerateEquipmentTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import equipment from a CSV stream.
|
||||
/// </summary>
|
||||
/// <param name="csvStream">CSV file stream</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <returns>Import result with success/error counts</returns>
|
||||
Task<CsvImportResultDto> ImportEquipmentAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for maintenance record imports.
|
||||
/// </summary>
|
||||
/// <returns>CSV file content as byte array</returns>
|
||||
byte[] GenerateMaintenanceTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import maintenance records from a CSV stream.
|
||||
/// </summary>
|
||||
/// <param name="csvStream">CSV file stream</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <returns>Import result with success/error counts</returns>
|
||||
Task<CsvImportResultDto> ImportMaintenanceAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for vendor imports.
|
||||
/// </summary>
|
||||
byte[] GenerateVendorTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import vendors from a CSV stream.
|
||||
/// Updates existing vendors matched by CompanyName; creates new ones otherwise.
|
||||
/// </summary>
|
||||
Task<CsvImportResultDto> ImportVendorsAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for shop worker imports.
|
||||
/// </summary>
|
||||
byte[] GenerateShopWorkerTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import shop workers from a CSV stream.
|
||||
/// Updates existing workers matched by Name; creates new ones otherwise.
|
||||
/// </summary>
|
||||
Task<CsvImportResultDto> ImportShopWorkersAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for prep service imports.
|
||||
/// </summary>
|
||||
byte[] GeneratePrepServiceTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import prep services from a CSV stream.
|
||||
/// Updates existing services matched by ServiceName; creates new ones otherwise.
|
||||
/// </summary>
|
||||
Task<CsvImportResultDto> ImportPrepServicesAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for expense imports.
|
||||
/// </summary>
|
||||
byte[] GenerateExpenseTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import expenses from a CSV stream.
|
||||
/// ExpenseAccountNumber and PaymentAccountNumber are resolved by Account.AccountNumber.
|
||||
/// VendorName and JobNumber are optional lookups. ExpenseNumber is auto-generated when blank.
|
||||
/// </summary>
|
||||
Task<CsvImportResultDto> ImportExpensesAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for Chart of Accounts imports.
|
||||
/// </summary>
|
||||
byte[] GenerateChartOfAccountsTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import Chart of Accounts entries from a CSV stream.
|
||||
/// Existing accounts matched by AccountNumber are updated; new ones are created.
|
||||
/// System accounts (IsSystem=true) are never modified by import.
|
||||
/// </summary>
|
||||
Task<CsvImportResultDto> ImportChartOfAccountsAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a CSV template file for invoice imports with headers matching the native export.
|
||||
/// </summary>
|
||||
byte[] GenerateInvoiceTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import invoice headers from a CSV stream. Customers are resolved by CustomerEmail then
|
||||
/// CustomerName. Duplicate detection uses InvoiceNumber as the unique key. Existing invoices
|
||||
/// are updated; new ones are created. Line items are not part of the CSV format.
|
||||
/// </summary>
|
||||
Task<CsvImportResultDto> ImportInvoicesAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>Generate a CSV template file for payment imports.</summary>
|
||||
byte[] GeneratePaymentTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import payment records from a CSV stream. Invoices are resolved by InvoiceNumber.
|
||||
/// Duplicates are detected by InvoiceNumber + PaymentDate + Amount and skipped.
|
||||
/// </summary>
|
||||
Task<CsvImportResultDto> ImportPaymentsAsync(Stream csvStream, int companyId);
|
||||
|
||||
/// <summary>Generate a CSV template file for purchase order imports.</summary>
|
||||
byte[] GeneratePurchaseOrderTemplate();
|
||||
|
||||
/// <summary>
|
||||
/// Import purchase order headers from a CSV stream. Vendors are resolved by company name.
|
||||
/// Existing POs matched by PoNumber are updated; new ones are created.
|
||||
/// </summary>
|
||||
Task<CsvImportResultDto> ImportPurchaseOrdersAsync(Stream csvStream, int companyId);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a file to attach to an outbound email.
|
||||
/// </summary>
|
||||
public record EmailAttachment(byte[] Data, string Filename, string ContentType);
|
||||
|
||||
public interface IEmailService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a plain or HTML email, optionally with a single file attachment (used for invoice
|
||||
/// and quote PDFs). For multiple attachments such as job photos use
|
||||
/// <see cref="SendEmailWithAttachmentsAsync"/>.
|
||||
/// </summary>
|
||||
Task<(bool Success, string? ErrorMessage)> SendEmailAsync(
|
||||
string toEmail,
|
||||
string toName,
|
||||
string subject,
|
||||
string plainTextBody,
|
||||
string? htmlBody = null,
|
||||
byte[]? attachmentData = null,
|
||||
string? attachmentFilename = null,
|
||||
string? attachmentContentType = null,
|
||||
string? replyToEmail = null,
|
||||
string? replyToName = null);
|
||||
|
||||
/// <summary>
|
||||
/// Sends an email with one or more file attachments (e.g. job photos). Callers are
|
||||
/// responsible for keeping total attachment size under SendGrid's 30 MB per-message limit.
|
||||
/// </summary>
|
||||
Task<(bool Success, string? ErrorMessage)> SendEmailWithAttachmentsAsync(
|
||||
string toEmail,
|
||||
string toName,
|
||||
string subject,
|
||||
string plainTextBody,
|
||||
string? htmlBody = null,
|
||||
IList<EmailAttachment>? attachments = null,
|
||||
string? replyToEmail = null,
|
||||
string? replyToName = null);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IEquipmentManualService
|
||||
{
|
||||
/// <summary>
|
||||
/// Save equipment manual to filesystem
|
||||
/// </summary>
|
||||
Task<(bool Success, string FilePath, string ErrorMessage)> SaveEquipmentManualAsync(IFormFile file, int companyId, int equipmentId);
|
||||
|
||||
/// <summary>
|
||||
/// Delete equipment manual from filesystem
|
||||
/// </summary>
|
||||
Task<(bool Success, string ErrorMessage)> DeleteEquipmentManualAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Get equipment manual from filesystem
|
||||
/// </summary>
|
||||
Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetEquipmentManualAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Check if equipment manual exists
|
||||
/// </summary>
|
||||
Task<bool> EquipmentManualExistsAsync(string filePath);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IFileService
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves an uploaded file to the specified subfolder
|
||||
/// </summary>
|
||||
/// <param name="file">The file to save</param>
|
||||
/// <param name="subfolder">Subfolder within uploads directory (e.g., "equipment-manuals")</param>
|
||||
/// <param name="allowedExtensions">Array of allowed file extensions (e.g., [".pdf", ".docx"])</param>
|
||||
/// <param name="maxFileSize">Maximum file size in bytes</param>
|
||||
/// <returns>Tuple containing success status, file path (relative to wwwroot), and error message if any</returns>
|
||||
Task<(bool Success, string FilePath, string ErrorMessage)> SaveFileAsync(
|
||||
IFormFile file,
|
||||
string subfolder,
|
||||
string[] allowedExtensions,
|
||||
long maxFileSize);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a file from the file system
|
||||
/// </summary>
|
||||
/// <param name="filePath">Relative path to the file (e.g., "uploads/equipment-manuals/file.pdf")</param>
|
||||
/// <returns>Tuple containing success status and error message if any</returns>
|
||||
Task<(bool Success, string ErrorMessage)> DeleteFileAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a file from the file system
|
||||
/// </summary>
|
||||
/// <param name="filePath">Relative path to the file</param>
|
||||
/// <returns>Tuple containing success status, file content, content type, and error message if any</returns>
|
||||
Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetFileAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a file exists
|
||||
/// </summary>
|
||||
/// <param name="filePath">Relative path to the file</param>
|
||||
/// <returns>True if file exists, false otherwise</returns>
|
||||
bool FileExists(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MIME content type for a file based on its extension
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file name</param>
|
||||
/// <returns>MIME content type</returns>
|
||||
string GetContentType(string fileName);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IInAppNotificationService
|
||||
{
|
||||
// Company-scoped notification — shown to all users of that company
|
||||
Task CreateAsync(int companyId, string title, string message, string notificationType,
|
||||
string? link = null, int? quoteId = null, int? invoiceId = null, int? customerId = null);
|
||||
|
||||
// Platform notification — shown only to SuperAdmins
|
||||
Task CreateForSuperAdminsAsync(string title, string message, string notificationType, string? link = null);
|
||||
|
||||
// Broadcast notification — one record per active company, shown to all tenant users
|
||||
Task CreateForAllCompaniesAsync(string title, string message, string notificationType, string? link = null);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public class InventoryAiLookupResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
// Identity
|
||||
public string? Manufacturer { get; set; }
|
||||
public string? ManufacturerPartNumber { get; set; }
|
||||
public string? ColorName { get; set; }
|
||||
public string? ColorCode { get; set; }
|
||||
public string? Description { get; set; }
|
||||
|
||||
// Coating specs
|
||||
public string? Finish { get; set; }
|
||||
public decimal? CureTemperatureF { get; set; }
|
||||
public int? CureTimeMinutes { get; set; }
|
||||
public string? ColorFamilies { get; set; } // comma-separated e.g. "Green,Blue"
|
||||
public bool? RequiresClearCoat { get; set; }
|
||||
|
||||
// Application properties
|
||||
public decimal? CoverageSqFtPerLb { get; set; } // typical ~80-120 sq ft/lb
|
||||
public decimal? TransferEfficiency { get; set; } // typical 50-75%
|
||||
public decimal? UnitCostPerLb { get; set; } // price per lb/unit if found in search results
|
||||
public string? VendorName { get; set; } // manufacturer/vendor name for dropdown matching
|
||||
public string? SpecPageUrl { get; set; } // URL of the product page that was fetched
|
||||
|
||||
public string? Reasoning { get; set; } // brief explanation of what was found
|
||||
}
|
||||
|
||||
public interface IInventoryAiLookupService
|
||||
{
|
||||
/// <summary>
|
||||
/// Search the web for powder coating product info and use AI to extract structured data.
|
||||
/// </summary>
|
||||
Task<InventoryAiLookupResult> LookupAsync(
|
||||
string? manufacturer,
|
||||
string? colorName,
|
||||
string? colorCode,
|
||||
string? partNumber);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IJobPhotoService
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a job photo to filesystem using a GUID-based filename for security
|
||||
/// Stores in [ContentRoot]/media/{companyId}/job-photos/{jobId}/{guid}.{ext}
|
||||
/// </summary>
|
||||
/// <param name="file">The photo file to upload</param>
|
||||
/// <param name="jobId">The job's ID</param>
|
||||
/// <param name="companyId">The company's ID</param>
|
||||
/// <param name="caption">Optional caption/note for the photo</param>
|
||||
/// <param name="photoType">Type of photo (Before, Progress, After, etc.)</param>
|
||||
/// <returns>Tuple with success status, relative file path, and error message if any</returns>
|
||||
Task<(bool Success, string FilePath, string ErrorMessage)> SaveJobPhotoAsync(
|
||||
IFormFile file,
|
||||
int jobId,
|
||||
int companyId,
|
||||
string? caption = null,
|
||||
JobPhotoType photoType = JobPhotoType.Progress);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a job photo from filesystem
|
||||
/// </summary>
|
||||
/// <param name="filePath">Relative path to the photo file</param>
|
||||
/// <returns>Tuple with success status and error message if any</returns>
|
||||
Task<(bool Success, string ErrorMessage)> DeleteJobPhotoAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a job photo
|
||||
/// </summary>
|
||||
/// <param name="filePath">Relative path to the photo</param>
|
||||
/// <returns>Tuple with success status, file bytes, content type, and error message</returns>
|
||||
Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetJobPhotoAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a job photo exists
|
||||
/// </summary>
|
||||
/// <param name="filePath">Relative path to the photo</param>
|
||||
/// <returns>True if exists, false otherwise</returns>
|
||||
Task<bool> JobPhotoExistsAsync(string filePath);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using PowderCoating.Application.DTOs.Accounting;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface ILedgerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all transactions that touch the given account within the date range,
|
||||
/// with a computed running balance.
|
||||
/// </summary>
|
||||
Task<AccountLedgerDto?> GetAccountLedgerAsync(int accountId, DateTime from, DateTime to);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Notify when a quote is created/sent. Handles both registered customers and prospects.
|
||||
/// Optionally attaches the quote PDF to the email.
|
||||
/// </summary>
|
||||
Task NotifyQuoteSentAsync(Quote quote, byte[]? pdfAttachment = null, string? pdfFilename = null);
|
||||
|
||||
/// <summary>
|
||||
/// Notify when a quote is approved by a customer.
|
||||
/// </summary>
|
||||
Task NotifyQuoteApprovedAsync(Quote quote);
|
||||
|
||||
/// <summary>
|
||||
/// Notify customer of a job status change. Also sends SMS when status is READY_FOR_PICKUP.
|
||||
/// </summary>
|
||||
Task NotifyJobStatusChangedAsync(Job job, string newStatusCode, string newStatusDisplayName);
|
||||
|
||||
/// <summary>
|
||||
/// Notify customer when a job is completed and ready for pickup.
|
||||
/// </summary>
|
||||
Task NotifyJobCompletedAsync(Job job);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a welcome/confirmation SMS after staff records verbal SMS consent.
|
||||
/// This message confirms enrollment and provides opt-out instructions per CTIA guidelines.
|
||||
/// </summary>
|
||||
Task NotifySmsConsentGrantedAsync(Customer customer);
|
||||
|
||||
/// <summary>
|
||||
/// Notify customer when an invoice has been sent.
|
||||
/// Optionally includes an online payment link in the email body.
|
||||
/// </summary>
|
||||
Task NotifyInvoiceSentAsync(Invoice invoice, byte[]? pdfAttachment = null, string? pdfFilename = null, string? paymentUrl = null);
|
||||
|
||||
/// <summary>
|
||||
/// Notify customer (internal) when a payment has been recorded on an invoice.
|
||||
/// </summary>
|
||||
Task NotifyPaymentReceivedAsync(Invoice invoice, Payment payment);
|
||||
|
||||
/// <summary>
|
||||
/// Notify the company (internal) when a customer approves or declines a quote via the self-service portal.
|
||||
/// </summary>
|
||||
Task NotifyQuoteActedByCustomerAsync(Quote quote, bool approved, string? declineReason);
|
||||
|
||||
/// <summary>
|
||||
/// Send a payment reminder to the customer for an overdue invoice.
|
||||
/// </summary>
|
||||
/// <param name="invoice">The overdue invoice (must have Customer loaded or CustomerId set).</param>
|
||||
/// <param name="daysOverdue">Number of days since the invoice was due.</param>
|
||||
Task NotifyPaymentReminderAsync(Invoice invoice, int daysOverdue);
|
||||
|
||||
/// <summary>
|
||||
/// Send an online payment receipt to the customer after a successful Stripe payment.
|
||||
/// </summary>
|
||||
Task NotifyOnlinePaymentReceivedAsync(Invoice invoice, decimal amountPaid, decimal surchargePaid, string paymentIntentId);
|
||||
|
||||
/// <summary>
|
||||
/// Notify the company (internal) when a customer pays a deposit online for a quote.
|
||||
/// </summary>
|
||||
Task NotifyDepositReceivedAsync(Quote quote, decimal amountPaid, decimal surchargePaid, string paymentIntentId);
|
||||
|
||||
/// <summary>
|
||||
/// Alert company staff when a Stripe chargeback (dispute) is opened on an invoice payment.
|
||||
/// </summary>
|
||||
Task NotifyChargebackAlertAsync(Invoice invoice, string disputeId, decimal amount, string reason);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using PowderCoating.Application.DTOs.Accounting;
|
||||
using PowderCoating.Application.DTOs.Company;
|
||||
using PowderCoating.Application.DTOs.GiftCertificate;
|
||||
using PowderCoating.Application.DTOs.Invoice;
|
||||
using PowderCoating.Application.DTOs.PurchaseOrder;
|
||||
using PowderCoating.Application.DTOs.Quote;
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IPdfService
|
||||
{
|
||||
Task<byte[]> GenerateQuotePdfAsync(
|
||||
QuoteDto quoteDto,
|
||||
byte[]? companyLogo,
|
||||
string? companyLogoContentType,
|
||||
CompanyInfoDto companyInfo,
|
||||
QuoteTemplateSettingsDto? template = null,
|
||||
byte[]? preparedByPhoto = null);
|
||||
|
||||
Task<byte[]> GenerateInvoicePdfAsync(
|
||||
InvoiceDto invoiceDto,
|
||||
byte[]? companyLogo,
|
||||
string? companyLogoContentType,
|
||||
CompanyInfoDto companyInfo,
|
||||
QuoteTemplateSettingsDto? template = null);
|
||||
|
||||
Task<byte[]> GeneratePurchaseOrderPdfAsync(
|
||||
PurchaseOrderDto po,
|
||||
byte[]? companyLogo,
|
||||
string? companyLogoContentType,
|
||||
CompanyInfoDto companyInfo);
|
||||
|
||||
Task<byte[]> GenerateCatalogPdfAsync(
|
||||
IEnumerable<IGrouping<CatalogCategory, CatalogItem>> itemsByCategory,
|
||||
string companyName,
|
||||
byte[]? companyLogo,
|
||||
string? companyLogoContentType);
|
||||
|
||||
Task<byte[]> GenerateProfitAndLossPdfAsync(ProfitAndLossDto dto);
|
||||
Task<byte[]> GenerateBalanceSheetPdfAsync(BalanceSheetDto dto);
|
||||
Task<byte[]> GenerateArAgingPdfAsync(ArAgingReportDto dto);
|
||||
Task<byte[]> GenerateSalesAndIncomePdfAsync(SalesIncomeReportDto dto);
|
||||
|
||||
Task<byte[]> GenerateGiftCertificatePdfAsync(
|
||||
GiftCertificateDto cert,
|
||||
byte[]? companyLogo,
|
||||
string? companyLogoContentType,
|
||||
CompanyInfoDto companyInfo);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IPlatformSettingsService
|
||||
{
|
||||
Task<string?> GetAsync(string key);
|
||||
Task SetAsync(string key, string? value, string? updatedBy = null);
|
||||
Task<IReadOnlyList<PlatformSetting>> GetAllAsync();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using PowderCoating.Application.DTOs.Powder;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IPowderInsightsService
|
||||
{
|
||||
Task<PowderDataReadiness> GetDataReadinessAsync(int companyId);
|
||||
Task<PowderInsightsDashboardDto> GetDashboardAsync(int companyId);
|
||||
Task<JobPowderSummaryDto?> GetJobPowderSummaryAsync(int jobId, int companyId);
|
||||
Task<RecordUsageResultDto> RecordActualUsageAsync(int jobItemCoatId, decimal actualLbs, string userId, int companyId, string? notes = null);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using PowderCoating.Application.DTOs.Quote;
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for calculating quote pricing based on operating costs, overhead, and profit margins
|
||||
/// </summary>
|
||||
public interface IPricingCalculationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the cost for a single coating layer on a quote item
|
||||
/// </summary>
|
||||
/// <param name="coat">The coating layer to price</param>
|
||||
/// <param name="itemSurfaceAreaSqFt">The surface area of the item (per single unit)</param>
|
||||
/// <param name="quantity">The quantity of items in the batch</param>
|
||||
/// <param name="coatIndex">The index of this coat (0 = first coat, 1 = second coat, etc.)</param>
|
||||
/// <param name="estimatedMinutesBase">The base estimated minutes for the item</param>
|
||||
/// <param name="companyId">The company ID for retrieving operating costs</param>
|
||||
/// <returns>Pricing breakdown for this specific coat</returns>
|
||||
Task<QuoteItemCoatPricingResult> CalculateCoatPriceAsync(
|
||||
CreateQuoteItemCoatDto coat,
|
||||
decimal itemSurfaceAreaSqFt,
|
||||
decimal quantity,
|
||||
int coatIndex,
|
||||
int estimatedMinutesBase,
|
||||
int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the price for a single quote item based on company operating costs
|
||||
/// </summary>
|
||||
/// <param name="item">The quote item to price</param>
|
||||
/// <param name="companyId">The company ID for retrieving operating costs</param>
|
||||
/// <returns>Detailed pricing breakdown for the item</returns>
|
||||
Task<QuoteItemPricingResult> CalculateQuoteItemPriceAsync(CreateQuoteItemDto item, int companyId, decimal? ovenCostOverride = null);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates total quote pricing including overhead, profit margin, discounts, and tax
|
||||
/// </summary>
|
||||
/// <param name="items">List of quote items to price</param>
|
||||
/// <param name="companyId">The company ID for retrieving operating costs</param>
|
||||
/// <param name="customerId">Optional customer ID for applying pricing tier discounts</param>
|
||||
/// <param name="manualTaxPercent">Optional manual tax percentage override</param>
|
||||
/// <param name="discountType">Type of quote-level discount to apply</param>
|
||||
/// <param name="discountValue">Value of quote-level discount (percentage or fixed amount)</param>
|
||||
/// <returns>Complete quote pricing breakdown</returns>
|
||||
Task<QuotePricingResult> CalculateQuoteTotalsAsync(
|
||||
List<CreateQuoteItemDto> items,
|
||||
int companyId,
|
||||
int? customerId = null,
|
||||
decimal? manualTaxPercent = null,
|
||||
string discountType = "None",
|
||||
decimal discountValue = 0,
|
||||
bool isRushJob = false,
|
||||
decimal? ovenCostOverride = null,
|
||||
int ovenBatches = 1,
|
||||
int? ovenCycleMinutes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the operating costs for a company
|
||||
/// </summary>
|
||||
/// <param name="companyId">The company ID</param>
|
||||
/// <returns>The company's operating costs or null if not configured</returns>
|
||||
Task<CompanyOperatingCosts?> GetOperatingCostsAsync(int companyId);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IProfilePhotoService
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a profile photo for a user
|
||||
/// Stores in [ContentRoot]/media/{companyId}/profile-photos/{userId}.{ext}
|
||||
/// </summary>
|
||||
/// <param name="file">The photo file to upload</param>
|
||||
/// <param name="userId">The user's ID</param>
|
||||
/// <param name="companyId">The company's ID</param>
|
||||
/// <returns>Tuple with success status, relative file path, and error message if any</returns>
|
||||
Task<(bool Success, string FilePath, string ErrorMessage)> SaveProfilePhotoAsync(
|
||||
IFormFile file,
|
||||
string userId,
|
||||
int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a user's profile photo
|
||||
/// </summary>
|
||||
/// <param name="filePath">Relative path to the photo file</param>
|
||||
/// <returns>Tuple with success status and error message if any</returns>
|
||||
Task<(bool Success, string ErrorMessage)> DeleteProfilePhotoAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a profile photo
|
||||
/// </summary>
|
||||
/// <param name="filePath">Relative path to the photo</param>
|
||||
/// <returns>Tuple with success status, file bytes, content type, and error message</returns>
|
||||
Task<(bool Success, byte[] FileContent, string ContentType, string ErrorMessage)> GetProfilePhotoAsync(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a profile photo exists
|
||||
/// </summary>
|
||||
/// <param name="filePath">Relative path to the photo</param>
|
||||
/// <returns>True if exists, false otherwise</returns>
|
||||
Task<bool> ProfilePhotoExistsAsync(string filePath);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using PowderCoating.Application.DTOs.QuickBooks;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for importing and exporting data in QuickBooks IIF (Intuit Interchange Format).
|
||||
/// Supports bidirectional synchronization of customers and catalog items.
|
||||
/// </summary>
|
||||
public interface IQuickBooksIifService
|
||||
{
|
||||
/// <summary>
|
||||
/// Export customers to QuickBooks Desktop IIF format.
|
||||
/// </summary>
|
||||
/// <param name="companyId">Company ID to filter customers</param>
|
||||
/// <returns>Tuple containing success status, file content, file name, and error message</returns>
|
||||
Task<(bool Success, byte[] FileContent, string FileName, string ErrorMessage)> ExportCustomersAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Export customers to QuickBooks Online CSV format.
|
||||
/// </summary>
|
||||
/// <param name="companyId">Company ID to filter customers</param>
|
||||
/// <returns>Tuple containing success status, file content, file name, and error message</returns>
|
||||
Task<(bool Success, byte[] FileContent, string FileName, string ErrorMessage)> ExportCustomersOnlineAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Export catalog items to QuickBooks Desktop IIF format as service items.
|
||||
/// </summary>
|
||||
/// <param name="companyId">Company ID to filter catalog items</param>
|
||||
/// <returns>Tuple containing success status, file content, file name, and error message</returns>
|
||||
Task<(bool Success, byte[] FileContent, string FileName, string ErrorMessage)> ExportCatalogItemsAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Export catalog items to QuickBooks Online CSV format as service items.
|
||||
/// </summary>
|
||||
/// <param name="companyId">Company ID to filter catalog items</param>
|
||||
/// <returns>Tuple containing success status, file content, file name, and error message</returns>
|
||||
Task<(bool Success, byte[] FileContent, string FileName, string ErrorMessage)> ExportCatalogItemsOnlineAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Import the Chart of Accounts from a QuickBooks Desktop IIF file.
|
||||
/// Creates new accounts or updates existing ones matched by AccountNumber (falls back to Name match).
|
||||
/// Sub-accounts (colon-delimited names like "Sales:Powder Coating") are linked to their parent.
|
||||
/// NONPOSTING accounts (Estimates, etc.) are skipped.
|
||||
/// </summary>
|
||||
/// <param name="file">IIF file exported from QB Desktop: Lists → Chart of Accounts → Export</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="userId">User ID for audit trail</param>
|
||||
/// <returns>Import result with counts and errors</returns>
|
||||
Task<ImportResultDto> ImportChartOfAccountsAsync(IFormFile file, int companyId, string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Import customers from a QuickBooks IIF file.
|
||||
/// Creates new customers or updates existing ones based on CompanyName match.
|
||||
/// </summary>
|
||||
/// <param name="file">IIF file to import</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="userId">User ID for audit trail</param>
|
||||
/// <returns>Import result with counts and errors</returns>
|
||||
Task<ImportResultDto> ImportCustomersAsync(IFormFile file, int companyId, string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Import catalog items from a QuickBooks IIF file.
|
||||
/// Creates new items or updates existing ones based on SKU match.
|
||||
/// </summary>
|
||||
/// <param name="file">IIF file to import</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="userId">User ID for audit trail</param>
|
||||
/// <returns>Import result with counts and errors</returns>
|
||||
Task<ImportResultDto> ImportCatalogItemsAsync(IFormFile file, int companyId, string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Validate a customer IIF file before importing.
|
||||
/// Checks file format, required fields, and basic data integrity.
|
||||
/// </summary>
|
||||
/// <param name="file">IIF file to validate</param>
|
||||
/// <returns>Validation result with errors</returns>
|
||||
Task<ValidationResultDto> ValidateCustomerFileAsync(IFormFile file);
|
||||
|
||||
/// <summary>
|
||||
/// Validate a catalog item IIF file before importing.
|
||||
/// Checks file format, required fields, and basic data integrity.
|
||||
/// </summary>
|
||||
/// <param name="file">IIF file to validate</param>
|
||||
/// <returns>Validation result with errors</returns>
|
||||
Task<ValidationResultDto> ValidateCatalogItemFileAsync(IFormFile file);
|
||||
|
||||
/// <summary>
|
||||
/// Export vendors to QuickBooks Desktop IIF format.
|
||||
/// </summary>
|
||||
Task<(bool Success, byte[] FileContent, string FileName, string ErrorMessage)> ExportVendorsAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Import vendors from a QuickBooks IIF file.
|
||||
/// Creates new vendors or updates existing ones based on CompanyName match.
|
||||
/// </summary>
|
||||
/// <param name="file">IIF file to import</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="userId">User ID for audit trail</param>
|
||||
/// <returns>Import result with counts and errors</returns>
|
||||
Task<ImportResultDto> ImportVendorsAsync(IFormFile file, int companyId, string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Import invoices from a QuickBooks Desktop "Customer Balance Detail" or
|
||||
/// "Transaction List by Customer" CSV report export.
|
||||
/// Requires customers (and ideally catalog items) to have been imported first.
|
||||
/// Creates one Invoice per unique QB invoice number, with InvoiceItems per line.
|
||||
/// Skips invoices whose QB invoice number has already been imported (ExternalReference match).
|
||||
/// </summary>
|
||||
/// <param name="file">CSV file exported from a QuickBooks report</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="userId">User ID for audit trail</param>
|
||||
/// <returns>Import result with counts and errors</returns>
|
||||
Task<ImportResultDto> ImportQbInvoicesFromCsvAsync(IFormFile file, int companyId, string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Import payment transactions from a QuickBooks Desktop "Transaction List by Customer" CSV report.
|
||||
/// Matches payments to previously-imported invoices by ExternalReference (QB invoice number).
|
||||
/// Updates invoice AmountPaid and Status (Paid / PartiallyPaid).
|
||||
/// Skips payments for invoices that are already fully paid.
|
||||
/// </summary>
|
||||
/// <param name="file">CSV file exported from QuickBooks Reports → Transaction List by Customer</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="userId">User ID for audit trail</param>
|
||||
/// <returns>Import result with counts and errors</returns>
|
||||
Task<ImportResultDto> ImportQbTransactionsFromCsvAsync(IFormFile file, int companyId, string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Import inventory stock levels from a QuickBooks Desktop "Inventory Valuation Summary" report CSV
|
||||
/// with the Preferred Vendor column added via Customize Report → Display → Pref Vendor.
|
||||
/// Creates new InventoryItems or updates existing ones matched by name.
|
||||
/// Records an Initial transaction for new items and an Adjustment transaction for updated items.
|
||||
/// Vendors matched by company name to existing vendor records; unmatched vendors populate Manufacturer only.
|
||||
/// </summary>
|
||||
/// <param name="file">CSV exported from QB Desktop: Reports → Inventory → Inventory Valuation Summary (with Pref Vendor column)</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="userId">User ID for audit trail</param>
|
||||
/// <returns>Import result with counts and errors</returns>
|
||||
Task<ImportResultDto> ImportQbInventoryValuationAsync(IFormFile file, int companyId, string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Import vendor bills from a QuickBooks Desktop "Vendor Balance Detail" CSV report.
|
||||
/// Each bill row becomes one Bill with one BillLineItem.
|
||||
/// Expense account matched by number prefix (e.g. "67100 · Rent Expense" → account 67100).
|
||||
/// AP account matched from the Split column; falls back to the first Accounts Payable account.
|
||||
/// Requires Chart of Accounts and Vendors to have been imported first.
|
||||
/// </summary>
|
||||
/// <param name="file">CSV exported from QB Desktop: Reports → Vendors & Payables → Vendor Balance Detail</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="userId">User ID for audit trail</param>
|
||||
/// <returns>Import result with counts and errors</returns>
|
||||
Task<ImportResultDto> ImportQbBillsAsync(IFormFile file, int companyId, string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Import vendor bill payments from a QuickBooks Desktop "Transaction List by Vendor" CSV report.
|
||||
/// Filters to "Bill Pmt -Check" and "Bill Pmt -CCard" rows only.
|
||||
/// Applies each payment to that vendor's open bills using FIFO (oldest bill first).
|
||||
/// One QB payment row may generate multiple BillPayment records if it spans several bills.
|
||||
/// Requires Chart of Accounts, Vendors, and Bills to have been imported first.
|
||||
/// </summary>
|
||||
/// <param name="file">CSV exported from QB Desktop: Reports → Vendors & Payables → Transaction List by Vendor</param>
|
||||
/// <param name="companyId">Company ID for multi-tenancy</param>
|
||||
/// <param name="userId">User ID for audit trail</param>
|
||||
/// <returns>Import result with counts and errors</returns>
|
||||
Task<ImportResultDto> ImportQbVendorPaymentsAsync(IFormFile file, int companyId, string userId);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IQuotePhotoService
|
||||
{
|
||||
/// <summary>Save an uploaded photo to temp storage. Returns (success, tempId, relativePath, error).</summary>
|
||||
Task<(bool Success, string TempId, string FilePath, string ErrorMessage)> SaveTempPhotoAsync(
|
||||
IFormFile file, int companyId);
|
||||
|
||||
/// <summary>Move a temp photo to permanent quote storage. Returns new relative path.</summary>
|
||||
Task<(bool Success, string FilePath, string ErrorMessage)> PromoteTempPhotoAsync(
|
||||
string tempId, int quoteId, int companyId);
|
||||
|
||||
/// <summary>Read photo bytes for AI processing.</summary>
|
||||
Task<(bool Success, byte[] Data, string ContentType)> ReadPhotoAsync(string filePath);
|
||||
|
||||
/// <summary>Delete a photo file.</summary>
|
||||
Task<bool> DeletePhotoAsync(string filePath);
|
||||
|
||||
/// <summary>Read all temp photos for a given tempId for AI processing.</summary>
|
||||
Task<List<(byte[] Data, string ContentType, string FileName)>> ReadTempPhotosAsync(string tempId);
|
||||
|
||||
/// <summary>Clean up all temp photos for a given tempId (on wizard cancel/abandon).</summary>
|
||||
Task CleanupTempAsync(string tempId);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface ISeedDataService
|
||||
{
|
||||
/// <summary>
|
||||
/// Seeds only lookup tables for a new company (called automatically on company creation).
|
||||
/// This includes job statuses, priorities, and quote statuses.
|
||||
/// </summary>
|
||||
/// <param name="companyId">The company to seed lookup data for</param>
|
||||
/// <returns>Result message indicating what was seeded</returns>
|
||||
Task<SeedDataResult> SeedCompanyLookupsAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Seeds initial data for a specific company
|
||||
/// </summary>
|
||||
/// <param name="companyId">The company to seed data for</param>
|
||||
/// <returns>Result message indicating what was seeded</returns>
|
||||
Task<SeedDataResult> SeedCompanyDataAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Seeds system-level data (roles, default company, SuperAdmins)
|
||||
/// </summary>
|
||||
/// <returns>Result message indicating what was seeded</returns>
|
||||
Task<SeedDataResult> SeedSystemDataAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Removes seeded demo data for a company based on the selected options.
|
||||
/// Matches records by their known seed identifiers (emails, SKUs, serial numbers, etc.).
|
||||
/// </summary>
|
||||
Task<SeedDataResult> RemoveSeedDataAsync(int companyId, RemoveSeedDataOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all companies for seeding
|
||||
/// </summary>
|
||||
Task<List<Company>> GetCompaniesAsync();
|
||||
}
|
||||
|
||||
public class RemoveSeedDataOptions
|
||||
{
|
||||
public bool Customers { get; set; }
|
||||
public bool InventoryItems { get; set; }
|
||||
public bool Equipment { get; set; }
|
||||
public bool Catalog { get; set; }
|
||||
public bool PricingTiers { get; set; }
|
||||
public bool OperatingCosts { get; set; }
|
||||
}
|
||||
|
||||
public class SeedDataResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public List<string> Details { get; set; } = new();
|
||||
public List<string> Warnings { get; set; } = new();
|
||||
public int ItemsSeeded { get; set; }
|
||||
public int ItemsSkipped { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface ISmsService
|
||||
{
|
||||
Task<(bool Success, string? ErrorMessage)> SendSmsAsync(string toPhone, string message);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IStorageMigrationService
|
||||
{
|
||||
Task<StorageMigrationResult> MigrateFilesystemToAzureAsync(
|
||||
string mediaBasePath,
|
||||
bool deleteLocalAfterMigration = false);
|
||||
}
|
||||
|
||||
public class StorageMigrationResult
|
||||
{
|
||||
public int Migrated { get; set; }
|
||||
public int Skipped { get; set; } // Already existed in Azure
|
||||
public int Failed { get; set; }
|
||||
public long BytesMigrated { get; set; }
|
||||
public List<MigratedFileEntry> Files { get; set; } = [];
|
||||
public List<string> Errors { get; set; } = [];
|
||||
public TimeSpan Duration { get; set; }
|
||||
|
||||
public bool HasErrors => Errors.Count > 0;
|
||||
public int Total => Migrated + Skipped + Failed;
|
||||
}
|
||||
|
||||
public class MigratedFileEntry
|
||||
{
|
||||
public string RelativePath { get; set; } = string.Empty;
|
||||
public string Container { get; set; } = string.Empty;
|
||||
public long FileSize { get; set; }
|
||||
public MigrationFileStatus Status { get; set; }
|
||||
}
|
||||
|
||||
public enum MigrationFileStatus
|
||||
{
|
||||
Migrated,
|
||||
Skipped,
|
||||
Failed
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IStripeConnectService
|
||||
{
|
||||
/// <summary>Returns the Stripe OAuth URL to begin the Standard Connect onboarding flow.</summary>
|
||||
string GetOAuthUrl(int companyId, string redirectUri);
|
||||
|
||||
/// <summary>Exchanges the OAuth authorization code for a StripeAccountId and saves it to the company.</summary>
|
||||
Task<(bool Success, string? ErrorMessage)> HandleOAuthCallbackAsync(string code, int companyId);
|
||||
|
||||
/// <summary>Deauthorizes the connected account and clears the company's Stripe Connect fields.</summary>
|
||||
Task<(bool Success, string? ErrorMessage)> DisconnectAsync(int companyId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Stripe PaymentIntent on the connected account for the given amount (in dollars).
|
||||
/// Returns the client secret needed by Stripe.js on the payment page.
|
||||
/// </summary>
|
||||
Task<(bool Success, string? ClientSecret, string? PaymentIntentId, string? ErrorMessage)> CreatePaymentIntentAsync(
|
||||
string connectedAccountId,
|
||||
decimal invoiceTotal,
|
||||
decimal surchargeAmount,
|
||||
string currency,
|
||||
string customerEmail,
|
||||
string invoiceNumber,
|
||||
int invoiceId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Stripe PaymentIntent on the connected account for a quote deposit.
|
||||
/// Metadata includes type=deposit so the webhook can distinguish it from invoice payments.
|
||||
/// </summary>
|
||||
Task<(bool Success, string? ClientSecret, string? PaymentIntentId, string? ErrorMessage)> CreateDepositPaymentIntentAsync(
|
||||
string connectedAccountId,
|
||||
decimal depositAmount,
|
||||
decimal surchargeAmount,
|
||||
string currency,
|
||||
string customerEmail,
|
||||
string quoteNumber,
|
||||
int quoteId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace PowderCoating.Application.Interfaces;
|
||||
|
||||
public interface IStripeService
|
||||
{
|
||||
Task<string> CreateCheckoutSessionAsync(int companyId, int newPlan, bool isAnnual, string successUrl, string cancelUrl);
|
||||
Task<string> CreateRegistrationCheckoutSessionAsync(int plan, bool isAnnual, string email, string companyName, string successUrl, string cancelUrl);
|
||||
Task FulfillCheckoutAsync(string sessionId);
|
||||
Task FulfillRegistrationCheckoutAsync(string sessionId, int companyId, int plan);
|
||||
Task SyncSubscriptionAsync(int companyId);
|
||||
Task<string> CreateCustomerPortalSessionAsync(string stripeCustomerId, string returnUrl);
|
||||
Task HandleWebhookAsync(string json, string stripeSignature);
|
||||
}
|
||||
Reference in New Issue
Block a user