Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -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 &amp; 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 &amp; 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);
}