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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions JobFlow.API/Controllers/PaymentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public async Task<IActionResult> Checkout([FromBody] PaymentSessionRequest reque
var org = await _organizationService.GetOrganiztionById(orgId);
if (org.IsFailure) return NotFound("Organization not found.");

var organization = org.Value;
var provider = Enum.IsDefined(typeof(PaymentProvider), organization.PaymentProvider)
? organization.PaymentProvider
: PaymentProvider.Stripe;

if (request.InvoiceId.HasValue)
{
var invoiceResult = await _invoiceService.GetInvoiceByIdAsync(request.InvoiceId.Value);
Expand Down Expand Up @@ -101,16 +106,25 @@ public async Task<IActionResult> Checkout([FromBody] PaymentSessionRequest reque
request.OrganizationId = invoice.OrganizationId;
request.OrganizationClientId = invoice.OrganizationClientId;
request.ProductName ??= $"Invoice {invoice.InvoiceNumber}";

if (Enum.IsDefined(typeof(PaymentProvider), invoice.PaymentProvider)
&& invoice.PaymentProvider != 0)
{
provider = invoice.PaymentProvider;
}
}

var processor = _processorFactory.GetProcessor(org.Value.PaymentProvider.ToString());
var processor = _processorFactory.GetProcessor(provider);

string checkoutUrl;
if (request.Mode == "subscription")
checkoutUrl = await processor.CreateSubscriptionCheckoutSessionAsync(request);
else
{
request.ConnectedAccountId = org.Value.StripeConnectAccountId;
if (provider == PaymentProvider.Stripe)
{
request.ConnectedAccountId = organization.StripeConnectAccountId;
}
var paymentIntent = await processor.CreatePaymentIntentAsync(request);

return Ok(new
Expand Down Expand Up @@ -227,7 +241,9 @@ public async Task<IActionResult> RefundPayment([FromBody] PaymentRefundRequestDt
Amount = request.Amount,
Currency = request.Currency,
Reason = request.Reason,
ConnectedAccountId = orgResult.Value.StripeConnectAccountId
ConnectedAccountId = request.Provider == PaymentProvider.Stripe
? orgResult.Value.StripeConnectAccountId
: null
});

return result.Success ? Ok(result) : BadRequest(result);
Expand All @@ -253,7 +269,9 @@ public async Task<IActionResult> AdjustPayment([FromBody] PaymentAdjustmentReque
Reason = request.Reason,
ProductName = request.ProductName,
InvoiceId = request.InvoiceId,
ConnectedAccountId = orgResult.Value.StripeConnectAccountId
ConnectedAccountId = request.Provider == PaymentProvider.Stripe
? orgResult.Value.StripeConnectAccountId
: null
});

return result.Success ? Ok(result) : BadRequest(result);
Expand All @@ -279,7 +297,9 @@ public async Task<IActionResult> CreateDeposit([FromBody] DepositPaymentRequestD
ProductName = request.ProductName,
Amount = request.Amount,
DepositAmount = request.Amount,
ConnectedAccountId = orgResult.Value.StripeConnectAccountId
ConnectedAccountId = request.Provider == PaymentProvider.Stripe
? orgResult.Value.StripeConnectAccountId
: null
});

return Ok(new
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public interface IPaymentSettings
{
decimal ApplicationFee { get; }
decimal ApplicationFee { get; }
}
2 changes: 1 addition & 1 deletion JobFlow.Business/Models/DTOs/JobDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class JobDto
public InvoicingWorkflow? InvoicingWorkflow { get; set; }
public Guid OrganizationClientId { get; set; }
public OrganizationClientDto? OrganizationClient { get; set; }

public IEnumerable<AssignmentDto>? Assignments { get; set; }
public bool HasAssignments => Assignments?.Any() == true;
}
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Business/Models/DTOs/OrganizationDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class OrganizationClientDto
public string? EmailAddress { get; set; }
public OrganizationDto? Organization { get; set; }
public string? FullName => $"{FirstName} {LastName}".Trim();

}

public class OrganizationDto
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Business/Onboarding/OnboardingStepKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ public static class OnboardingStepKeys
public const string CreateInvoice = "create_invoice";
public const string SendInvoice = "send_invoice";
public const string ReceivePayment = "receive_payment";
public const string ConnectStripe = "connect_stripe";
public const string ConnectStripe = "connect_stripe";
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class PaymentSessionRequest
public string? CancelUrl { get; set; }
public string? Email { get; set; }
public Guid? OrgId { get; set; }

public Guid? InvoiceId { get; set; }

// Subscription-specific
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Business/Services/AssignmentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public AssignmentService(
_assignmentAssignees = unitOfWork.RepositoryOf<AssignmentAssignee>();
_employees = unitOfWork.RepositoryOf<Employee>();
_jobs = unitOfWork.RepositoryOf<Job>();

_mapper = mapper;
_onboardingService = onboardingService;
_workflowSettings = workflowSettings;
Expand Down
16 changes: 14 additions & 2 deletions JobFlow.Business/Services/InvoiceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ public class InvoiceService : IInvoiceService
private readonly INotificationService _notifications;
private readonly IOrganizationClientPortalService _clientPortal;
private readonly IInvoiceRealtimeNotifier? _realtimeNotifier;
private readonly IOrganizationService _organizationService;
private readonly IUnitOfWork unitOfWork;

public InvoiceService(
ILogger<InvoiceService> logger,
IUnitOfWork unitOfWork,
IOrganizationService organizationService,
IOnboardingService onboardingService,
IInvoiceNumberGenerator numberGenerator,
INotificationService notifications,
Expand All @@ -35,6 +37,7 @@ public InvoiceService(
{
this.logger = logger;
this.unitOfWork = unitOfWork;
_organizationService = organizationService;
invoices = unitOfWork.RepositoryOf<Invoice>();
estimates = unitOfWork.RepositoryOf<Estimate>();
clients = unitOfWork.RepositoryOf<OrganizationClient>();
Expand Down Expand Up @@ -94,6 +97,15 @@ public async Task<Result<Invoice>> UpsertInvoiceAsync(Invoice model)
// Calculate TotalAmount manually since it's not mapped
model.TotalAmount = model.LineItems?.Sum(li => li.Quantity * li.UnitPrice) ?? 0;

if (!Enum.IsDefined(typeof(PaymentProvider), model.PaymentProvider)
|| model.PaymentProvider == 0)
{
var orgResult = await _organizationService.GetOrganiztionById(model.OrganizationId);
model.PaymentProvider = orgResult.IsSuccess
? (orgResult.Value.PaymentProvider == 0 ? PaymentProvider.Stripe : orgResult.Value.PaymentProvider)
: PaymentProvider.Stripe;
}

if (exists)
invoices.Update(model);
else
Expand All @@ -109,7 +121,7 @@ public async Task<Result<Invoice>> UpsertInvoiceAsync(Invoice model)
}

await unitOfWork.SaveChangesAsync();

await _onboardingService.MarkStepCompleteAsync(
model.OrganizationId,
OnboardingStepKeys.CreateInvoice
Expand All @@ -129,7 +141,7 @@ public async Task<Result> DeleteInvoiceAsync(Guid id)
await unitOfWork.SaveChangesAsync();
return Result.Success();
}

public async Task MarkInvoiceSentAsync(Guid invoiceId)
{
var invoice = await invoices
Expand Down
28 changes: 14 additions & 14 deletions JobFlow.Business/Services/JobService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ public async Task<Result<IEnumerable<JobDto>>> GetJobsAsync(Guid organizationId)
.Where(j => j.OrganizationClient.OrganizationId == organizationId)
.OrderByDescending(j => j.CreatedAt)
.ToListAsync();

var dto = returnedJobs.Select(e => new JobDto
{
Id = e.Id,
OrganizationClientId = e.OrganizationClient.Id,
Title = e.Title,
Title = e.Title,
Comments = e.Comments,
LifecycleStatus = e.LifecycleStatus,
InvoicingWorkflow = e.InvoicingWorkflow,
Expand All @@ -126,11 +126,11 @@ public async Task<Result<IEnumerable<JobDto>>> GetJobsAsync(Guid organizationId)
ScheduledEnd = a.ScheduledEnd,
ActualEnd = a.ActualEnd,
ActualStart = a.ActualStart,
Id = a.Id,
JobId = e.Id,
JobTitle = e.Title,
Id = a.Id,
JobId = e.Id,
JobTitle = e.Title,
Status = a.Status,
OrganizationClientId = e.OrganizationClientId,
OrganizationClientId = e.OrganizationClientId,
JobLifecycleStatus = e.LifecycleStatus,
Assignees = a.AssignmentAssignees
.Select(assignee => new AssignmentAssigneeDto
Expand All @@ -145,16 +145,16 @@ public async Task<Result<IEnumerable<JobDto>>> GetJobsAsync(Guid organizationId)
}),
OrganizationClient = new OrganizationClientDto
{
OrganizationId = e.OrganizationClient.OrganizationId,
FirstName = e.OrganizationClient.FirstName,
LastName = e.OrganizationClient.LastName,
EmailAddress = e.OrganizationClient.EmailAddress,
OrganizationId = e.OrganizationClient.OrganizationId,
FirstName = e.OrganizationClient.FirstName,
LastName = e.OrganizationClient.LastName,
EmailAddress = e.OrganizationClient.EmailAddress,
PhoneNumber = e.OrganizationClient.PhoneNumber,
Address1 = e.OrganizationClient.Address1,
Address1 = e.OrganizationClient.Address1,
Address2 = e.OrganizationClient.Address2,
City = e.OrganizationClient.City,
State = e.OrganizationClient.State,
ZipCode = e.OrganizationClient.ZipCode
City = e.OrganizationClient.City,
State = e.OrganizationClient.State,
ZipCode = e.OrganizationClient.ZipCode
}
}).ToList();
return Result.Success<IEnumerable<JobDto>>(dto);
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Business/Services/OrganizationClientService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public async Task<Result<OrganizationClient>> UpsertClient(OrganizationClient mo
}

await unitOfWork.SaveChangesAsync();

if (!exists)
{
await onboardingService.MarkStepCompleteAsync(
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Business/Services/OrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class OrganizationService : IOrganizationService
private readonly IRepository<SubscriptionRecord> _subscriptions;

public OrganizationService(
IUnitOfWork unitOfWork,
IUnitOfWork unitOfWork,
ILogger<OrganizationService> logger,
IOnboardingService onboardingService)
{
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Business/Services/PaymentProfileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public async Task<Result<CustomerPaymentProfile>> CreateAsync(Guid ownerId, Paym
OwnerType = ownerType,
Provider = provider,
ProviderCustomerId = providerCustomerId,
CreatedAt = DateTime.UtcNow
CreatedAt = DateTime.UtcNow
};

paymentProfiles.Add(profile);
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Business/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public async Task<Result<User>> GetUserByFirebaseUid(string uid)
return Result.Success(user);
}


private static string ResolvePrimaryRole(User user)
{
return user.UserRoles
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Domain/Models/Assignment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
public class Assignment : Entity
{
public Guid JobId { get; set; }
public virtual Job Job { get; set; }

Check warning on line 8 in JobFlow.Domain/Models/Assignment.cs

View workflow job for this annotation

GitHub Actions / Build

Non-nullable property 'Job' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

// Window vs exact is semantic; both use ScheduledStart/End
public ScheduleType ScheduleType { get; set; } = ScheduleType.Window;
Expand All @@ -15,7 +15,7 @@

public DateTimeOffset? ActualStart { get; set; }
public DateTimeOffset? ActualEnd { get; set; }

public AssignmentStatus Status { get; set; } = AssignmentStatus.Scheduled;

// Optional: assignment-level override location (job location can differ from client address)
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Domain/Models/Invoice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class Invoice : Entity
public decimal AmountPaid { get; set; }
public decimal BalanceDue => TotalAmount - AmountPaid;
public InvoiceStatus Status { get; set; }

public PaymentProvider PaymentProvider { get; set; }
public string? ExternalPaymentId { get; set; }
public DateTimeOffset? PaidAt { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion JobFlow.Domain/Models/Job.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class Job : Entity
public InvoicingWorkflow? InvoicingWorkflow { get; set; }
public string? Title { get; set; }
public string? Comments { get; set; }

public Guid OrganizationClientId { get; set; }
public virtual OrganizationClient OrganizationClient { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ public void Configure(EntityTypeBuilder<Job> builder)

builder.Property(e => e.Comments)
.HasMaxLength(2000);

builder.Property(j => j.LifecycleStatus)
.HasConversion<int>()
.IsRequired();

builder.Property(j => j.InvoicingWorkflow)
.HasConversion<int>();

// ✅ Relationship with OrganizationClient
builder.HasOne(j => j.OrganizationClient)
.WithMany(c => c.Jobs) // assuming you added ICollection<Job> Jobs to OrganizationClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
using JobFlow.Infrastructure.ExternalServices.ConfigurationInterfaces;
using System.Net.Http.Json;
using Square;

using Square.Checkout.PaymentLinks;
using Microsoft.Extensions.Hosting;


namespace JobFlow.Infrastructure.PaymentGateways.SquarePayment;
Expand All @@ -16,18 +16,23 @@ public class SquarePaymentProcessor : IPaymentProcessor, IPaymentOperationsProce
private readonly SquareClient _client;
private readonly string _accessToken;
private readonly string _locationId;
private readonly string _baseUrl;

public SquarePaymentProcessor(ISquareSettings settings)
public SquarePaymentProcessor(ISquareSettings settings, IHostEnvironment hostEnvironment)
{
ArgumentNullException.ThrowIfNull(settings);
if (string.IsNullOrWhiteSpace(settings.AccessToken))
throw new InvalidOperationException("Square access token is not configured.");

_accessToken = settings.AccessToken;

_baseUrl = hostEnvironment.IsDevelopment()
? "https://connect.squareupsandbox.com"
: "https://connect.squareup.com";

_client = new SquareClient(settings.AccessToken, new ClientOptions
{
BaseUrl = "https://connect.squareupsandbox.com"
BaseUrl = _baseUrl
});

_locationId = settings.LocationId ?? string.Empty;
Expand Down Expand Up @@ -61,9 +66,15 @@ public async Task<string> CreateCheckoutSessionAsync(PaymentSessionRequest reque
var paymentLinkRequest = new CreatePaymentLinkRequest
{
QuickPay = quickPay,
IdempotencyKey = idempotencyKey
IdempotencyKey = idempotencyKey,
Description = request.ProductName
};

if (request.InvoiceId.HasValue)
{
paymentLinkRequest.PaymentNote = $"invoiceId={request.InvoiceId.Value}";
}

try
{
var result = await _client.Checkout.PaymentLinks.CreateAsync(paymentLinkRequest);
Expand Down Expand Up @@ -158,7 +169,7 @@ private HttpClient CreateApiClient()
{
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://connect.squareupsandbox.com/")
BaseAddress = new Uri($"{_baseUrl}/")
};
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task<PaymentSessionResult> CreatePaymentIntentAsync(
},

ApplicationFeeAmount = applicationFee,

TransferData = new PaymentIntentTransferDataOptions
{
Destination = request.ConnectedAccountId
Expand Down
Loading
Loading