Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Acs Bundle Laravel Package

answear/acs-bundle

Symfony bundle for ACS pickup point integration. Configure API credentials via YAML, then use ParcelShopsService to fetch parcel shops by country (Greece or Cyprus) and optional kind, returning ParcelShop DTOs. Includes clear exceptions for service and response errors.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. There is no ServiceProvider, no auto-discovery in Laravel, and the README's config/bundles.php snippet does not apply. Before you install anything, decide which of two paths you're on:

Path A — Reuse the classes, wire the DI yourself (fastest to evaluate).

Add the repository (it is not on Packagist; the README's composer require [email protected]/... is a git URL, not a package name):

// composer.json
"repositories": [
    { "type": "vcs", "url": "https://github.com/answear/acs-bundle" }
]
composer require answear/acs-bundle

Then read AnswearAcsBundle\DependencyInjection\AnswearAcsExtension::load() first — that is the only place that tells you exactly which constructor arguments ParcelShopsService expects (typically a Guzzle client, a Symfony Serializer instance, and the config array). Mirror that in a Laravel provider:

// config/answear_acs.php
return [
    'api_key'          => env('ACS_API_KEY'),
    'company_id'       => env('ACS_COMPANY_ID'),
    'company_password' => env('ACS_COMPANY_PASSWORD'),
    'user_id'          => env('ACS_USER_ID'),
    'user_password'     => env('ACS_USER_PASSWORD'),
    'language'         => env('ACS_LANGUAGE', 'GR'),
];
// app/Providers/AcsServiceProvider.php
public function register(): void
{
    $this->app->singleton(ParcelShopsService::class, function ($app) {
        $config = $app['config']->get('answear_acs');

        // exact args per AnswearAcsExtension::load() — adjust after reading it
        return new ParcelShopsService(
            AcsClientFactory::make($config),   // Guzzle client with timeout
            new Serializer(),                  // symfony/serializer
            $config,
        );
    });
}

Path B — Port it (recommended, ~500 LOC, half a day). The bundle is small. Copy Service/, Response/DTO/, Exception/, and the enum into app/Acs/ and replace Guzzle + Symfony Serializer with Laravel's Http facade and json_decode. You keep the DTO shapes, drop the Symfony weight, and own the timeout/retry story. This is what most Laravel teams end up doing.

First use case: a checkout "pick a parcel shop" dropdown. Expose GET /acs/parcel-shops?country=GR, return the DTO list as JSON, and persist the chosen shop ID on the order — the ID is what you'd later hand to whatever creates the actual ACS shipment (this bundle does not do shipping or tracking).

Implementation Patterns

Wrap the vendor service behind your own interface. Do not let Answear\AcsBundle\* types leak into controllers, jobs, or Blade. One thin repository:

namespace App\Services\Acs;

use Answear\AcsBundle\Exception\MalformedResponse;
use Answear\AcsBundle\Exception\ServiceUnavailable;
use Answear\AcsBundle\Service\ParcelShopsService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

class AcsParcelShopRepository
{
    public function __construct(private ParcelShopsService $client) {}

    /** @return \Answear\AcsBundle\Response\DTO\ParcelShop[] */
    public function forCountry(string $iso2, ?int $kind = null): array
    {
        $country = \Answear\AcsBundle\Enum\CountryIdEnum::from($iso2); // verify enum path

        return Cache::remember(
            "acs.parcelshops.{$iso2}." . ($kind ?? 'all'),
            now()->addHours(12),
            function () use ($country, $kind) {
                try {
                    return $this->client->getList($country, $kind);
                } catch (ServiceUnavailable $e) {
                    Log::warning('ACS unavailable', ['prev' => $e->getPrevious()?->getMessage()]);
                    throw new AcsUnavailable($e->getMessage(), previous: $e);
                } catch (MalformedResponse $e) {
                    Log::error('ACS schema drift', ['message' => $e->getMessage()]);
                    throw new AcsMalformed($e->getMessage(), previous: $e);
                }
            }
        );
    }
}

Cache aggressively. Parcel shop lists for GR/CY change on the order of weeks, not minutes. Cache for hours, warm it with a scheduled command, and always serve from cache on the checkout request path — never block a cart page on a live ACS call.

Map DTOs to an API resource. ACS property names and shapes are not your front-end contract. Do the translation in a JsonResource or a plain array_map, so upgrading the bundle (which has had BC breaks: 2.x → 3.x dropped PHP 7.4/8.0/8.1 and Symfony <6; 4.0.0 bumps to PHP 8.4) doesn't ripple into your JS.

return collect($shops)->map(fn ($s) => [
    'id'      => $s->id,           // verify field names on ParcelShop
    'name'    => $s->name,
    'address' => trim("{$s->street} {$s->streetNumber}, {$s->zip} {$s->city}"),
    'kind'    => $s->kind,
])->values();

Persist the choice, not the object. Store acs_parcel_shop_id on the order. When it's time to create the shipment, you call ACS's separate create-order API with that ID — the bundle does not cover that.

Testing: mock at your repository boundary, not inside Guzzle. Bind a fake in the container:

$this->mock(AcsParcelShopRepository::class, fn ($m) => $m
    ->shouldReceive('forCountry')->with('GR')->andReturn($this->fakeShops()));

If you go Path B (Laravel Http), Http::fake() gives you clean record/replay of ACS responses.

Version pinning matters more than usual. These are Laravel/PHP floor constraints, not preference:

Bundle PHP Symfony Works with
4.0.0 8.4 7 Laravel 11/12 on PHP 8.4
3.0.x 8.2 6/7 Laravel 10/11
2.1.x 8.1 6.4 Laravel 10

Gotchas and Tips

  • The README has a copy/paste bug. The config file is named answear_acs.yaml, but the sample body starts with answear_gls:. Use your own key (answear_acs) in Laravel — you're writing the provider/port yourself anyway.
  • Credentials live in five keys — apiKey, companyId, companyPassword, userId, userPassword. All five are required; a missing one surfaces as a 401 wrapped in ServiceUnavailable. Put them in .env, run php artisan config:cache, and rotate via your secret store, not the repo.
  • ServiceUnavailable hides the real cause. It wraps every GuzzleException — DNS failure, timeout, 401, 403, 500 all look the same. Always log/inspect getPrevious(). A 401 during prod debugging is almost always a config typo, not an ACS outage.
  • MalformedResponse means the schema moved. Log the raw body at debug level — without it you'll be guessing which field changed. Consider a Log::error + alert rather than a silent 500.
  • Timeout was only added in 2.1.1. If you're pinned below that, ACS latency can hang a request. On Path B, set Http::timeout(5)->connectTimeout(2) explicitly. On Path A, build the Guzzle client yourself with those options.
  • No PSR-3 logging dependency as of 3.0.1. Requests are invisible. Wrap the repository call with your own Log::info (with duration) or let Telescope capture it; otherwise you're blind when ACS starts rate-limiting.
  • CountryIdEnum is GR/CY only. Don't build multi-country logic around it — the enum will reject anything else. Gate the checkout option behind in_array($country, ['GR', 'CY']).
  • kind = null means "all shop kinds." If you want lockers only, either pass the ACS kind code or filter on the DTO after fetch. Don't assume the API returns only staffed shops.
  • The bundle exposes almost no extension points — no client interface, no events, no middleware hook. If you need retry, custom headers, or logging, do it in your wrapper (Path A) or in your ported client (Path B). Keeping the integration behind AcsParcelShopRepository means replacing the bundle later is a one-file change.
  • The language default is GR. If your storefront is English, verify the DTO's address strings respect that; field-level i18n is not guaranteed.
  • Maturity signal is weak. Zero stars, zero dependents, single-maintainer repo, last release tagged 2026-06-22. Treat this as "vendor code you happen to pull from GitHub" — pin the exact version, and don't be surprised if you're the one patching it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource