diff --git a/.env.example b/.env.example index 1d1c890..a232809 100644 --- a/.env.example +++ b/.env.example @@ -48,6 +48,7 @@ AUTH_GOOGLE_SECRET= AUTH_INTUIT_CLIENT_ID= AUTH_INTUIT_SECRET_KEY= +INTUIT_VERIFYTOKEN= PAYPAL_MODE=sandbox PAYPAL_SANDBOX_CLIENT_ID= diff --git a/app/Console/Commands/AccountingPaymentGet.php b/app/Console/Commands/AccountingPaymentGet.php new file mode 100644 index 0000000..690cb7f --- /dev/null +++ b/app/Console/Commands/AccountingPaymentGet.php @@ -0,0 +1,58 @@ +argument('siteid')); + Config::set('site',$site); + + $uo = User::where('email',$this->argument('user'))->singleOrFail(); + + $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); + if (! ($to=$so->token($uo))) + abort(500,sprintf('Unknown Tokens for [%s]',$uo->email)); + + try { + $api = $to->API(); + dump($api->getPaymentQuery($this->argument('id'))); + + } catch (ConnectException|ConnectionIssueException $e) { + $this->error($e->getMessage()); + + return Command::FAILURE; + } + } +} \ No newline at end of file diff --git a/app/Console/Commands/AccountingPaymentSync.php b/app/Console/Commands/AccountingPaymentSync.php new file mode 100644 index 0000000..8b9dda0 --- /dev/null +++ b/app/Console/Commands/AccountingPaymentSync.php @@ -0,0 +1,50 @@ +argument('siteid')); + Config::set('site',$site); + + $uo = User::where('email',$this->argument('user'))->singleOrFail(); + + $so = ProviderOauth::where('name',$this->argument('provider'))->singleOrFail(); + if (! ($to=$so->token($uo))) + abort(500,sprintf('Unknown Tokens for [%s]',$uo->email)); + + $api = $to->API(); + foreach ($api->getPayments() as $acc) + Job::dispatchSync($to,$acc); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/AccountingController.php b/app/Http/Controllers/AccountingController.php index 3a1db7b..50f51c2 100644 --- a/app/Http/Controllers/AccountingController.php +++ b/app/Http/Controllers/AccountingController.php @@ -2,9 +2,7 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Log; use App\Models\ProviderOauth; use App\Models\User; @@ -38,9 +36,4 @@ class AccountingController extends Controller ->transform(function($item,$value) { return ['id'=>$value,'value'=>$item]; }) ->values(); } - - public function webhook(Request $request) - { - Log::channel('webhook')->debug('Webhook event',['request'=>$request]); - } } \ No newline at end of file diff --git a/app/Jobs/AccountingPaymentSync.php b/app/Jobs/AccountingPaymentSync.php new file mode 100644 index 0000000..c235e8b --- /dev/null +++ b/app/Jobs/AccountingPaymentSync.php @@ -0,0 +1,136 @@ +pmi = $pmi; + $this->to = $to; + } + + /** + * Execute the job. + * + * @return void + * @throws \Exception + */ + public function handle() + { + // See if we are already linked + if (($x=$this->to->provider->payments->where('pivot.ref',$this->pmi->id))->count() === 1) { + $o = $x->pop(); + + } else { + // Find the account + $ao = Account::select('accounts.*') + ->join('account__provider',['account__provider.account_id'=>'accounts.id']) + ->where('provider_oauth_id',$this->to->provider_oauth_id) + ->where('ref',$this->pmi->account_ref) + ->single(); + + if (! $ao) { + Log::alert(sprintf('%s:Account not found for payment [%s:%d]',self::LOGKEY,$this->pmi->id,$this->pmi->account_ref)); + return; + } + + // Create the payment + $o = new Payment; + $o->account_id = $ao->id; + $o->site_id = $ao->site_id; // @todo Automatically determine + } + + // Update the payment details + $o->paid_at = $this->pmi->date_paid; + $o->active = TRUE; + $o->checkout_id = 2; // @todo + $o->total_amt = $this->pmi->total_amt; + $o->notes = 'Imported from Intuit'; + $o->save(); + + Log::info(sprintf('%s:Recording payment [%s:%3.2f]',self::LOGKEY,$this->pmi->id,$this->pmi->total_amt)); + + $o->providers()->syncWithoutDetaching([ + $this->to->provider->id => [ + 'ref' => $this->pmi->id, + 'synctoken' => $this->pmi->synctoken, + 'created_at'=>Carbon::create($this->pmi->created_at), + 'updated_at'=>Carbon::create($this->pmi->updated_at), + 'site_id'=>$this->to->site_id, + ], + ]); + + // Load the invoice that this payment pays + $invoices = collect(); + foreach ($this->pmi->lines() as $item => $amount) { + $invoice = Invoice::select('invoices.*') + ->join('invoice__provider',['invoice__provider.invoice_id'=>'invoices.id']) + ->where('provider_oauth_id',$this->to->provider_oauth_id) + ->where('ref',$item) + ->single(); + + $invoices->put($item,$invoice); + } + + // Delete existing paid invoices that are no longer paid + foreach ($o->items as $pio) + if ($invoices->pluck('id')->search($pio->invoice_id) === FALSE) + $pio->delete(); + + // Update payment items + foreach ($this->pmi->lines() as $item => $amount) { + if (! $invoices->has($item)) { + Log::alert(sprintf('%s:Invoice [%s] not recorded, payment not assigned',self::LOGKEY,$item)); + continue; + } + + $io = $invoices->get($item); + + // If the payment item already exists + if (($x=$o->items->where('invoice_id',$io->id))->count()) { + $pio = $x->pop(); + + } else { + $pio = new PaymentItem; + $pio->invoice_id = $io->id; + $pio->site_id = $io->site_id; + } + + $pio->amount = $amount; + $o->items()->save($pio); + } + + Log::alert(sprintf('%s:Payment updated [%s:%s]',self::LOGKEY,$o->id,$this->pmi->id)); + } +} \ No newline at end of file diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 3434074..c5e4c28 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\DB; use Leenooks\Traits\ScopeActive; use App\Interfaces\IDs; +use App\Traits\ProviderRef; use App\Traits\PushNew; /** @@ -24,7 +25,7 @@ use App\Traits\PushNew; */ class Payment extends Model implements IDs { - use PushNew,ScopeActive; + use PushNew,ScopeActive,ProviderRef; protected $dates = [ 'paid_at', @@ -75,6 +76,13 @@ class Payment extends Model implements IDs return $this->hasMany(PaymentItem::class); } + public function providers() + { + return $this->belongsToMany(ProviderOauth::class,'payment__provider') + ->where('payment__provider.site_id',$this->site_id) + ->withPivot('ref','synctoken','created_at','updated_at'); + } + /* SCOPES */ /** diff --git a/app/Models/ProviderOauth.php b/app/Models/ProviderOauth.php index 41a14b2..66e0334 100644 --- a/app/Models/ProviderOauth.php +++ b/app/Models/ProviderOauth.php @@ -31,6 +31,13 @@ class ProviderOauth extends Model ->withPivot('ref','synctoken','created_at','updated_at'); } + public function payments() + { + return $this->belongsToMany(Payment::class,'payment__provider') + ->where('payment__provider.site_id',$this->site_id) + ->withPivot('ref','synctoken','created_at','updated_at'); + } + public function taxes() { return $this->belongsToMany(Tax::class,'tax__provider') diff --git a/composer.lock b/composer.lock index 82ec208..7f7108d 100644 --- a/composer.lock +++ b/composer.lock @@ -1289,25 +1289,25 @@ }, { "name": "firebase/php-jwt", - "version": "v6.4.0", + "version": "v6.5.0", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224" + "reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/4dd1e007f22a927ac77da5a3fbb067b42d3bc224", - "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/e94e7353302b0c11ec3cfff7180cd0b1743975d2", + "reference": "e94e7353302b0c11ec3cfff7180cd0b1743975d2", "shasum": "" }, "require": { - "php": "^7.1||^8.0" + "php": "^7.4||^8.0" }, "require-dev": { "guzzlehttp/guzzle": "^6.5||^7.4", - "phpspec/prophecy-phpunit": "^1.1", - "phpunit/phpunit": "^7.5||^9.5", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", "psr/cache": "^1.0||^2.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" @@ -1346,9 +1346,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.4.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.5.0" }, - "time": "2023-02-09T21:01:23+00:00" + "time": "2023-05-12T15:47:07+00:00" }, { "name": "fruitcake/laravel-cors", @@ -3681,11 +3681,11 @@ }, { "name": "leenooks/intuit", - "version": "0.1.3", + "version": "0.1.4", "source": { "type": "git", "url": "https://dev.dege.au/leenooks/intuit", - "reference": "2f1b34a806c19877379c588cf1b53f8ac3474d42" + "reference": "2f75c7dd1fa6179e25f06ba2a617993858ccb7eb" }, "require": { "jenssegers/model": "^1.5" @@ -3715,7 +3715,7 @@ "laravel", "leenooks" ], - "time": "2023-05-12T12:41:53+00:00" + "time": "2023-05-13T11:17:36+00:00" }, { "name": "leenooks/laravel", diff --git a/config/services.php b/config/services.php index 06283cb..b01ebe1 100644 --- a/config/services.php +++ b/config/services.php @@ -46,6 +46,7 @@ return [ 'provider' => [ 'intuit' => [ 'api'=> \Intuit\API::class, + 'verifytoken' => env('INTUIT_VERIFYTOKEN'), ] ], diff --git a/database/migrations/2023_05_13_142113_accounting_payment.php b/database/migrations/2023_05_13_142113_accounting_payment.php new file mode 100644 index 0000000..644df17 --- /dev/null +++ b/database/migrations/2023_05_13_142113_accounting_payment.php @@ -0,0 +1,38 @@ +timestamps(); + $table->integer('payment_id')->unsigned(); + $table->integer('provider_oauth_id')->unsigned(); + $table->integer('site_id')->unsigned(); + $table->string('ref'); + $table->integer('synctoken'); + + $table->foreign(['payment_id','site_id'])->references(['id','site_id'])->on('payments'); + $table->foreign(['provider_oauth_id','site_id'])->references(['id','site_id'])->on('provider_oauth'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('payment__provider'); + } +}; diff --git a/database/migrations/2023_05_13_200404_unique_payment_item.php b/database/migrations/2023_05_13_200404_unique_payment_item.php new file mode 100644 index 0000000..70a4726 --- /dev/null +++ b/database/migrations/2023_05_13_200404_unique_payment_item.php @@ -0,0 +1,32 @@ +unique(['site_id','payment_id','invoice_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('payment_items', function (Blueprint $table) { + $table->dropUnique(['site_id','payment_id','invoice_id']); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index b02c2ee..30b80e0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,7 @@ 'auth:api'], function() { Route::any('/intuit/accounting/list',[AccountingController::class,'list']); }); -// @todo Take the specific 'intuit' out of this url, to enable other accounting methods -Route::any('/intuit/webhook',[AccountingController::class,'webhook']); \ No newline at end of file +Route::any('/intuit/webhook',[Webhook::class,'webhook']); \ No newline at end of file