Skip to content
Open
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
347 changes: 346 additions & 1 deletion Services/Stripe/PaymentProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,362 @@ public function __construct()
$this->settings = $settings["secret"];
}

/**
* Get customer billing address (tries multiple relationship patterns)
*/
private function getCustomerBilling($customer)
{
if (!$customer) {
return null;
}

// Try different relationship patterns used by InvoiceShelf
try {
// Try addresses() relationship
if (method_exists($customer, 'addresses')) {
$address = $customer->addresses()->where('type', 'billing')->first();
if ($address) {
return $address;
}
// Fallback to first address
$address = $customer->addresses()->first();
if ($address) {
return $address;
}
}
} catch (\Exception $e) {
// Continue to next attempt
}

try {
// Try billingAddress() relationship
if (method_exists($customer, 'billingAddress')) {
return $customer->billingAddress;
}
} catch (\Exception $e) {
// Continue to next attempt
}

try {
// Try direct address property
if (property_exists($customer, 'billing_address_street_1')) {
// Address fields are directly on customer
return $customer;
}
} catch (\Exception $e) {
// Continue
}

return null;
}

/**
* Get country code from customer billing
*/
private function getCountryCode($customer)
{
$billing = $this->getCustomerBilling($customer);

if (!$billing) {
return null;
}

// Try to get country code from country_id
$countryId = $billing->country_id ?? $billing->billing_country_id ?? null;

if ($countryId) {
try {
$country = \App\Models\Country::find($countryId);
if ($country) {
// Try various field names for 2-letter ISO code
$possibleFields = ['iso2', 'iso_2', 'code', 'short_code', 'country_code'];

foreach ($possibleFields as $field) {
if (!empty($country->$field) && strlen($country->$field) === 2) {
return strtoupper($country->$field);
}
}
}
} catch (\Exception $e) {
// Continue if Country model lookup fails
}
}

// Try direct country field
$countryField = $billing->country ?? $billing->billing_country ?? null;
if ($countryField && strlen($countryField) === 2) {
return strtoupper($countryField);
}

return null;
}

/**
* Search for existing Stripe customer by email
*/
private function findExistingStripeCustomer($email)
{
if (!$email) {
return null;
}

try {
$response = Http::withHeaders([
'Accept' => 'application/json',
])
->withToken($this->settings)
->get("https://api.stripe.com/v1/customers?email=" . urlencode($email) . "&limit=1");

if ($response->status() === 200) {
$data = $response->json();
if (!empty($data['data']) && count($data['data']) > 0) {
return $data['data'][0]['id'];
}
}
} catch (\Exception $e) {
// If search fails, we'll create new customer
}

return null;
}

/**
* Create or retrieve Stripe Customer
*/
private function getOrCreateStripeCustomer($customer, $invoice)
{
if (!$customer || !$customer->email) {
return null;
}

// IMPORTANT: Search for existing customer first to prevent duplicates
$existingCustomerId = $this->findExistingStripeCustomer($customer->email);
if ($existingCustomerId) {
return $existingCustomerId;
}

// Build customer parameters
$customerParams = [
'email' => $customer->email,
];

// Set name (both individual and business name for Stripe)
if ($customer->name) {
$customerParams['name'] = $customer->name;
}

if ($customer->phone) {
$customerParams['phone'] = $customer->phone;
}

// Get customer's company name from custom field
$customerCompanyName = $this->getCustomerCompanyName($customer);
if ($customerCompanyName) {
$customerParams['description'] = $customerCompanyName;
// Also set as metadata
$customerParams['metadata[company_name]'] = $customerCompanyName;
}

// Add address if available
$billing = $this->getCustomerBilling($customer);
if ($billing) {
$address = [];

// Try different field naming patterns
$street1 = $billing->address_street_1 ?? $billing->billing_address_street_1 ??
$billing->street_1 ?? $billing->address_1 ?? null;
$street2 = $billing->address_street_2 ?? $billing->billing_address_street_2 ??
$billing->street_2 ?? $billing->address_2 ?? null;
$city = $billing->city ?? $billing->billing_city ?? null;
$state = $billing->state ?? $billing->billing_state ?? null;
$zip = $billing->zip ?? $billing->postal_code ?? $billing->billing_zip ?? null;

if ($street1) {
$address['line1'] = $street1;
}
if ($street2) {
$address['line2'] = $street2;
}
if ($city) {
$address['city'] = $city;
}
if ($state) {
$address['state'] = $state;
}
if ($zip) {
$address['postal_code'] = $zip;
}

// Get country code
$countryCode = $this->getCountryCode($customer);
if ($countryCode) {
$address['country'] = $countryCode;
}

if (!empty($address)) {
foreach ($address as $key => $value) {
$customerParams["address[{$key}]"] = $value;
}
}
}

// Add InvoiceShelf metadata
$customerParams['metadata[invoiceshelf_customer_id]'] = $customer->id;
$customerParams['metadata[source]'] = 'InvoiceShelf';

// Build request body
$bodyParts = [];
foreach ($customerParams as $key => $value) {
$bodyParts[] = urlencode($key) . '=' . urlencode($value);
}
$requestBody = implode('&', $bodyParts);

// Create Stripe customer
$response = Http::withHeaders([
'Accept' => 'application/json',
])
->withToken($this->settings)
->withBody($requestBody, 'application/x-www-form-urlencoded')
->post("https://api.stripe.com/v1/customers");

if ($response->status() === 200) {
return $response->json()['id'];
}

return null;
}

/**
* Get customer's company name from custom field
*/
private function getCustomerCompanyName($customer)
{
if (!$customer) {
return null;
}

try {
// Try to get custom field value
$customField = $customer->fields()
->where('model_type', 'Customer')
->whereHas('customField', function($query) {
$query->where('slug', 'CUSTOM_CUSTOMER_COMPANY_NAME')
->orWhere('slug', 'custom_customer_company_name');
})
->first();

// InvoiceShelf stores custom field values in different columns based on type
// For Input type fields, it uses string_answer
if ($customField) {
if (!empty($customField->string_answer)) {
return $customField->string_answer;
} elseif (!empty($customField->value)) {
return $customField->value;
}
}
} catch (\Exception $e) {
// If custom fields aren't available, gracefully continue
}

return null;
}

public function generatePayment(Company $company, $invoice)
{
$currency = Currency::find($invoice->currency_id);
$total = $invoice->total;

// Get customer data from invoice
$customer = $invoice->user ?? $invoice->customer ?? null;

// Build the payment intent parameters
$paymentParams = [
'amount' => $total,
'currency' => $currency->code,
];

// Add enhanced description with invoice number and customer email
if ($customer && $customer->email) {
$paymentParams['description'] = "Payment on Invoice #{$invoice->invoice_number} - {$customer->email}";
} else {
$paymentParams['description'] = "Payment on Invoice #{$invoice->invoice_number}";
}

// Create or get Stripe customer and attach to payment intent
$stripeCustomerId = $this->getOrCreateStripeCustomer($customer, $invoice);
if ($stripeCustomerId) {
$paymentParams['customer'] = $stripeCustomerId;

// Set receipt_email for automatic receipts
if ($customer->email) {
$paymentParams['receipt_email'] = $customer->email;
}
}

// Get customer's company name from custom field
$customerCompanyName = $this->getCustomerCompanyName($customer);

// Add comprehensive metadata
$metadata = [
'invoice_id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'service_provider_id' => $company->id,
'service_provider_name' => $company->name,
];

if ($customer) {
if ($customer->name) {
$metadata['customer_name'] = $customer->name;
}
if ($customer->email) {
$metadata['customer_email'] = $customer->email;
}
if ($customer->phone) {
$metadata['customer_phone'] = $customer->phone;
}
if ($customer->id) {
$metadata['customer_id'] = $customer->id;
}
// Add customer's company name if available
if ($customerCompanyName) {
$metadata['customer_company_name'] = $customerCompanyName;
}
}

// Add metadata to payment params
foreach ($metadata as $key => $value) {
$paymentParams["metadata[{$key}]"] = $value;
}

// Build the request body with proper URL encoding
// IMPORTANT: Convert boolean values to strings for Stripe API
$bodyParts = [];
foreach ($paymentParams as $key => $value) {
if (is_bool($value)) {
// Convert boolean to string "true" or "false"
$bodyParts[] = urlencode($key) . '=' . ($value ? 'true' : 'false');
} elseif (is_array($value)) {
// Handle nested arrays
foreach ($value as $subKey => $subValue) {
if (is_bool($subValue)) {
$bodyParts[] = urlencode("{$key}[{$subKey}]") . '=' . ($subValue ? 'true' : 'false');
} else {
$bodyParts[] = urlencode("{$key}[{$subKey}]") . '=' . urlencode($subValue);
}
}
} else {
$bodyParts[] = urlencode($key) . '=' . urlencode($value);
}
}
$requestBody = implode('&', $bodyParts);

// Create payment intent with enhanced data
$response = Http::withHeaders([
'Accept' => 'application/json',
'Accept-Language' => 'en_US'
])
->withToken($this->settings)
->withBody("amount={$total}&currency={$currency->code}", 'application/x-www-form-urlencoded')
->withBody($requestBody, 'application/x-www-form-urlencoded')
->post("https://api.stripe.com/v1/payment_intents");

if ($response->status() !== 200) {
Expand Down