From 7749fb1a0b437f8f93ea226c3305e00092566ec3 Mon Sep 17 00:00:00 2001
From: James Cole <thegrumpydictator@gmail.com>
Date: Thu, 28 Jun 2018 17:02:13 +0200
Subject: [PATCH] Expand API for journal links.

---
 .../V1/Controllers/JournalLinkController.php  | 209 ++++++++++++++++++
 app/Api/V1/Requests/CurrencyRequest.php       |   2 +-
 app/Api/V1/Requests/JournalLinkRequest.php    |  68 ++++++
 app/Models/LinkType.php                       |  11 +-
 app/Models/Note.php                           |   2 +
 app/Models/TransactionJournalLink.php         |  12 +
 .../LinkType/LinkTypeRepository.php           | 122 +++++++++-
 .../LinkType/LinkTypeRepositoryInterface.php  |  44 +++-
 app/Transformers/JournalLinkTransformer.php   | 146 ++++++++++++
 app/Transformers/LinkTypeTransformer.php      |  89 ++++++++
 resources/lang/en_US/firefly.php              |   2 +
 resources/views/admin/link/show.twig          |   4 +-
 routes/api.php                                |  13 ++
 13 files changed, 707 insertions(+), 17 deletions(-)
 create mode 100644 app/Api/V1/Controllers/JournalLinkController.php
 create mode 100644 app/Api/V1/Requests/JournalLinkRequest.php
 create mode 100644 app/Transformers/JournalLinkTransformer.php
 create mode 100644 app/Transformers/LinkTypeTransformer.php

diff --git a/app/Api/V1/Controllers/JournalLinkController.php b/app/Api/V1/Controllers/JournalLinkController.php
new file mode 100644
index 0000000000..c54b42484e
--- /dev/null
+++ b/app/Api/V1/Controllers/JournalLinkController.php
@@ -0,0 +1,209 @@
+<?php
+/**
+ * JournalLinkController.php
+ * Copyright (c) 2018 thegrumpydictator@gmail.com
+ *
+ * This file is part of Firefly III.
+ *
+ * Firefly III is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Firefly III is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace FireflyIII\Api\V1\Controllers;
+
+use FireflyIII\Api\V1\Requests\JournalLinkRequest;
+use FireflyIII\Exceptions\FireflyException;
+use FireflyIII\Models\TransactionJournalLink;
+use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
+use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
+use FireflyIII\Transformers\JournalLinkTransformer;
+use FireflyIII\User;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Pagination\LengthAwarePaginator;
+use League\Fractal\Manager;
+use League\Fractal\Pagination\IlluminatePaginatorAdapter;
+use League\Fractal\Resource\Collection as FractalCollection;
+use League\Fractal\Resource\Item;
+use League\Fractal\Serializer\JsonApiSerializer;
+
+class JournalLinkController extends Controller
+{
+    /** @var JournalRepositoryInterface */
+    private $journalRepository;
+    /** @var LinkTypeRepositoryInterface */
+    private $repository;
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->middleware(
+            function ($request, $next) {
+                /** @var User $user */
+                $user = auth()->user();
+
+                $this->repository        = app(LinkTypeRepositoryInterface::class);
+                $this->journalRepository = app(JournalRepositoryInterface::class);
+
+                $this->repository->setUser($user);
+                $this->journalRepository->setUser($user);
+
+                return $next($request);
+            }
+        );
+    }
+
+    /**
+     * Delete the resource.
+     *
+     * @param string $object
+     *
+     * @return JsonResponse
+     */
+    public function delete(string $object): JsonResponse
+    {
+        // todo delete object.
+
+        return response()->json([], 204);
+    }
+
+    /**
+     * List all of them.
+     *
+     * @param Request $request
+     *
+     * @return JsonResponse]
+     */
+    public function index(Request $request): JsonResponse
+    {
+
+
+        // create some objects:
+        $manager = new Manager;
+        $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1';
+
+        // read type from URI
+        $name = $request->get('name') ?? null;
+
+        // types to get, page size:
+        $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
+
+        $linkType = $this->repository->findByName($name);
+
+        // get list of accounts. Count it and split it.
+        $collection   = $this->repository->getJournalLinks($linkType);
+        $count        = $collection->count();
+        $journalLinks = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
+
+        // make paginator:
+        $paginator = new LengthAwarePaginator($journalLinks, $count, $pageSize, $this->parameters->get('page'));
+        $paginator->setPath(route('api.v1.journal_links.index') . $this->buildParams());
+
+        // present to user.
+        $manager->setSerializer(new JsonApiSerializer($baseUrl));
+        $resource = new FractalCollection($journalLinks, new JournalLinkTransformer($this->parameters), 'journal_links');
+        $resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
+
+        return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
+
+    }
+
+    /**
+     * List single resource.
+     *
+     * @param Request                $request
+     * @param TransactionJournalLink $journalLink
+     *
+     * @return JsonResponse
+     */
+    public function show(Request $request, TransactionJournalLink $journalLink): JsonResponse
+    {
+        $manager = new Manager;
+
+        // add include parameter:
+        $include = $request->get('include') ?? '';
+        $manager->parseIncludes($include);
+
+        $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1';
+        $manager->setSerializer(new JsonApiSerializer($baseUrl));
+        $resource = new Item($journalLink, new JournalLinkTransformer($this->parameters), 'journal_links');
+
+        return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
+
+    }
+
+    /**
+     * Store new object.
+     *
+     * @param JournalLinkRequest $request
+     *
+     * @return JsonResponse
+     * @throws FireflyException
+     */
+    public function store(JournalLinkRequest $request): JsonResponse
+    {
+        $manager = new Manager;
+
+        // add include parameter:
+        $include = $request->get('include') ?? '';
+        $manager->parseIncludes($include);
+
+        $data    = $request->getAll();
+        $inward  = $this->journalRepository->findNull($data['inward_id'] ?? 0);
+        $outward = $this->journalRepository->findNull($data['outward_id'] ?? 0);
+        if (null === $inward || null === $outward) {
+            throw new FireflyException('Source or destination is NULL.');
+        }
+        $data['direction'] = 'inward';
+
+        $journalLink = $this->repository->storeLink($data, $inward, $outward);
+
+        $resource = new Item($journalLink, new JournalLinkTransformer($this->parameters), 'journal_links');
+
+        return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
+
+    }
+
+    /**
+     * @param JournalLinkRequest     $request
+     * @param TransactionJournalLink $journalLink
+     *
+     * @return JsonResponse
+     * @throws FireflyException
+     */
+    public function update(JournalLinkRequest $request, TransactionJournalLink $journalLink): JsonResponse
+    {
+        $manager = new Manager;
+
+        // add include parameter:
+        $include = $request->get('include') ?? '';
+        $manager->parseIncludes($include);
+
+
+        $data            = $request->getAll();
+        $data['inward']  = $this->journalRepository->findNull($data['inward_id'] ?? 0);
+        $data['outward'] = $this->journalRepository->findNull($data['outward_id'] ?? 0);
+        if (null === $data['inward'] || null === $data['outward']) {
+            throw new FireflyException('Source or destination is NULL.');
+        }
+        $data['direction'] = 'inward';
+        $journalLink       = $this->repository->updateLink($journalLink, $data);
+
+        $resource = new Item($journalLink, new JournalLinkTransformer($this->parameters), 'journal_links');
+
+        return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
+
+    }
+}
\ No newline at end of file
diff --git a/app/Api/V1/Requests/CurrencyRequest.php b/app/Api/V1/Requests/CurrencyRequest.php
index 8e2edfeda9..6c63c83ddc 100644
--- a/app/Api/V1/Requests/CurrencyRequest.php
+++ b/app/Api/V1/Requests/CurrencyRequest.php
@@ -41,7 +41,7 @@ class CurrencyRequest extends Request
     /**
      * @return array
      */
-    public function getAll()
+    public function getAll(): array
     {
         return [
             'name'           => $this->string('name'),
diff --git a/app/Api/V1/Requests/JournalLinkRequest.php b/app/Api/V1/Requests/JournalLinkRequest.php
new file mode 100644
index 0000000000..e52d5b7484
--- /dev/null
+++ b/app/Api/V1/Requests/JournalLinkRequest.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * JournalLinkRequest.php
+ * Copyright (c) 2018 thegrumpydictator@gmail.com
+ *
+ * This file is part of Firefly III.
+ *
+ * Firefly III is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Firefly III is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace FireflyIII\Api\V1\Requests;
+
+
+/**
+ *
+ * Class JournalLinkRequest
+ */
+class JournalLinkRequest extends Request
+{
+    /**
+     * @return bool
+     */
+    public function authorize(): bool
+    {
+        // Only allow authenticated users
+        return auth()->check();
+    }
+
+    /**
+     * @return array
+     */
+    public function getAll(): array
+    {
+        return [
+            'link_type_id' => $this->integer('link_type_id'),
+            'inward_id'    => $this->integer('inward_id'),
+            'outward_id'   => $this->integer('outward_id'),
+            'notes'        => $this->string('notes'),
+        ];
+    }
+
+    /**
+     * @return array
+     */
+    public function rules(): array
+    {
+        return [
+            'link_type_id' => 'required|exists:link_types,id',
+            'inward_id'    => 'required|belongsToUser:transaction_journals,id',
+            'outward_id'   => 'required|belongsToUser:transaction_journals,id',
+            'notes'        => 'between:0,65000',
+        ];
+    }
+
+}
\ No newline at end of file
diff --git a/app/Models/LinkType.php b/app/Models/LinkType.php
index 04dd71f3aa..a39fc05041 100644
--- a/app/Models/LinkType.php
+++ b/app/Models/LinkType.php
@@ -22,12 +22,21 @@ declare(strict_types=1);
 
 namespace FireflyIII\Models;
 
+use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Model;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
- * @property int $journalCount
+ * @property int    $journalCount
+ * @property string $inward
+ * @property string $outward
+ * @property string $name
+ * @property bool   $editable
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ * @property int $id
  * Class LinkType
+ *
  */
 class LinkType extends Model
 {
diff --git a/app/Models/Note.php b/app/Models/Note.php
index 316b65089e..81ebc4a9ff 100644
--- a/app/Models/Note.php
+++ b/app/Models/Note.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Models;
 
 use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
 
 /**
  * Class Note.
@@ -36,6 +37,7 @@ use Illuminate\Database\Eloquent\Model;
  */
 class Note extends Model
 {
+    use SoftDeletes;
     /**
      * The attributes that should be casted to native types.
      *
diff --git a/app/Models/TransactionJournalLink.php b/app/Models/TransactionJournalLink.php
index af47153390..aa5146b3e7 100644
--- a/app/Models/TransactionJournalLink.php
+++ b/app/Models/TransactionJournalLink.php
@@ -22,6 +22,7 @@ declare(strict_types=1);
 
 namespace FireflyIII\Models;
 
+use Carbon\Carbon;
 use Crypt;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -29,6 +30,17 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
  * Class TransactionJournalLink.
+ *
+ * @property int                $id
+ * @property Carbon             $created_at
+ * @property Carbon             $updated_at
+ * @property string             $comment
+ * @property TransactionJournal $source
+ * @property TransactionJournal $destination
+ * @property LinkType           $linkType
+ * @property int                $link_type_id
+ * @property int                $source_id
+ * @property int                $destination_id
  */
 class TransactionJournalLink extends Model
 {
diff --git a/app/Repositories/LinkType/LinkTypeRepository.php b/app/Repositories/LinkType/LinkTypeRepository.php
index a4eb99cff6..76fa74d748 100644
--- a/app/Repositories/LinkType/LinkTypeRepository.php
+++ b/app/Repositories/LinkType/LinkTypeRepository.php
@@ -22,6 +22,7 @@ declare(strict_types=1);
 
 namespace FireflyIII\Repositories\LinkType;
 
+use Exception;
 use FireflyIII\Exceptions\FireflyException;
 use FireflyIII\Models\LinkType;
 use FireflyIII\Models\Note;
@@ -94,6 +95,20 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface
         return $linkType;
     }
 
+    /**
+     * @param string|null $name
+     *
+     * @return LinkType|null
+     */
+    public function findByName(string $name = null): ?LinkType
+    {
+        if (null === $name) {
+            return null;
+        }
+
+        return LinkType::where('name', $name)->first();
+    }
+
     /**
      * Check if link exists between journals.
      *
@@ -110,6 +125,24 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface
         return $count + $opposingCount > 0;
     }
 
+    /**
+     * See if such a link already exists (and get it).
+     *
+     * @param LinkType           $linkType
+     * @param TransactionJournal $inward
+     * @param TransactionJournal $outward
+     *
+     * @return TransactionJournalLink|null
+     */
+    public function findSpecificLink(LinkType $linkType, TransactionJournal $inward, TransactionJournal $outward): ?TransactionJournalLink
+    {
+        return TransactionJournalLink
+            ::where('link_type_id', $linkType->id)
+            ->where('source_id', $inward->id)
+            ->where('destination_id', $outward->id)->first();
+
+    }
+
     /**
      * @return Collection
      */
@@ -118,6 +151,30 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface
         return LinkType::orderBy('name', 'ASC')->get();
     }
 
+    /**
+     * Returns all the journal links (of a specific type).
+     *
+     * @param $linkType
+     *
+     * @return Collection
+     */
+    public function getJournalLinks(LinkType $linkType = null): Collection
+    {
+        $query = TransactionJournalLink
+            ::leftJoin('transaction_journals as source_journals', 'journal_links.source_id', '=', 'source_journals.id')
+            ->leftJoin('transaction_journals as dest_journals', 'journal_links.destination_id', '=', 'dest_journals.id')
+            ->where('source_journals.user_id', $this->user->id)
+            ->where('dest_journals.user_id', $this->user->id)
+            ->whereNull('source_journals.deleted_at')
+            ->whereNull('dest_journals.deleted_at');
+
+        if (null !== $linkType) {
+            $query->where('journal_links.link_type_id', $linkType->id);
+        }
+
+        return $query->get(['journal_links.*']);
+    }
+
     /**
      * Return list of existing connections.
      *
@@ -169,35 +226,42 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface
      * Store link between two journals.
      *
      * @param array              $information
-     * @param TransactionJournal $left
-     * @param TransactionJournal $right
+     * @param TransactionJournal $inward
+     * @param TransactionJournal $outward
      *
      * @return mixed
      * @throws FireflyException
      */
-    public function storeLink(array $information, TransactionJournal $left, TransactionJournal $right): TransactionJournalLink
+    public function storeLink(array $information, TransactionJournal $inward, TransactionJournal $outward): TransactionJournalLink
     {
         $linkType = $this->find((int)($information['link_type_id'] ?? 0));
         if (null === $linkType->id) {
             throw new FireflyException(sprintf('Link type #%d cannot be resolved to an actual link type', $information['link_type_id'] ?? 0));
         }
+
+        // might exist already:
+        $existing = $this->findSpecificLink($linkType, $inward, $outward);
+        if (null !== $existing) {
+            return $existing;
+        }
+
         $link = new TransactionJournalLink;
         $link->linkType()->associate($linkType);
         if ('inward' === $information['direction']) {
-            Log::debug(sprintf('Link type is inwards ("%s"), so %d is source and %d is destination.', $linkType->inward, $left->id, $right->id));
-            $link->source()->associate($left);
-            $link->destination()->associate($right);
+            Log::debug(sprintf('Link type is inwards ("%s"), so %d is source and %d is destination.', $linkType->inward, $inward->id, $outward->id));
+            $link->source()->associate($inward);
+            $link->destination()->associate($outward);
         }
 
         if ('outward' === $information['direction']) {
-            Log::debug(sprintf('Link type is inwards ("%s"), so %d is source and %d is destination.', $linkType->outward, $right->id, $left->id));
-            $link->source()->associate($right);
-            $link->destination()->associate($left);
+            Log::debug(sprintf('Link type is inwards ("%s"), so %d is source and %d is destination.', $linkType->outward, $outward->id, $inward->id));
+            $link->source()->associate($outward);
+            $link->destination()->associate($inward);
         }
         $link->save();
 
         // make note in noteable:
-        if (\strlen($information['notes']) > 0) {
+        if (\strlen((string)$information['notes']) > 0) {
             $dbNote = $link->notes()->first();
             if (null === $dbNote) {
                 $dbNote = new Note();
@@ -240,4 +304,42 @@ class LinkTypeRepository implements LinkTypeRepositoryInterface
 
         return $linkType;
     }
+
+    /**
+     * Update an existing transaction journal link.
+     *
+     * @param TransactionJournalLink $journalLink
+     * @param array                  $data
+     *
+     * @return TransactionJournalLink
+     */
+    public function updateLink(TransactionJournalLink $journalLink, array $data): TransactionJournalLink
+    {
+        $journalLink->source_id      = $data['inward']->id;
+        $journalLink->destination_id = $data['outward']->id;
+        $journalLink->link_type_id   = $data['link_type_id'];
+        $journalLink->save();
+        /** @var Note $note */
+        $note = $journalLink->notes()->first();
+        // delete note:
+        if (null !== $note && '' === $data['notes']) {
+            try {
+                $note->delete();
+            } catch (Exception $e) {
+                Log::debug(sprintf('Could not delete note for journal link: %s', $e->getMessage()));
+            }
+        }
+        // create note:
+        if (null === $note && '' !== $data['notes']) {
+            $note = new Note;
+            $note->noteable()->associate($journalLink);
+        }
+        // update note
+        if ('' !== $data['notes']) {
+            $note->text = $data['notes'];
+            $note->save();
+        }
+
+        return $journalLink;
+    }
 }
diff --git a/app/Repositories/LinkType/LinkTypeRepositoryInterface.php b/app/Repositories/LinkType/LinkTypeRepositoryInterface.php
index 19ae1c8846..a0c8d2c636 100644
--- a/app/Repositories/LinkType/LinkTypeRepositoryInterface.php
+++ b/app/Repositories/LinkType/LinkTypeRepositoryInterface.php
@@ -57,10 +57,20 @@ interface LinkTypeRepositoryInterface
     /**
      * @param int $id
      *
+     * @deprecated
      * @return LinkType
      */
     public function find(int $id): LinkType;
 
+    /**
+     * Find link type by name.
+     *
+     * @param string|null $name
+     *
+     * @return LinkType|null
+     */
+    public function findByName(string $name = null): ?LinkType;
+
     /**
      * Check if link exists between journals.
      *
@@ -71,11 +81,29 @@ interface LinkTypeRepositoryInterface
      */
     public function findLink(TransactionJournal $one, TransactionJournal $two): bool;
 
+    /**
+     * See if such a link already exists (and get it).
+     *
+     * @param LinkType           $linkType
+     * @param TransactionJournal $inward
+     * @param TransactionJournal $outward
+     *
+     * @return TransactionJournalLink|null
+     */
+    public function findSpecificLink(LinkType $linkType, TransactionJournal $inward, TransactionJournal $outward): ?TransactionJournalLink;
+
     /**
      * @return Collection
      */
     public function get(): Collection;
 
+    /**
+     * @param LinkType|null $linkType
+     *
+     * @return Collection
+     */
+    public function getJournalLinks(LinkType $linkType = null): Collection;
+
     /**
      * Return list of existing connections.
      *
@@ -96,12 +124,12 @@ interface LinkTypeRepositoryInterface
      * Store link between two journals.
      *
      * @param array              $information
-     * @param TransactionJournal $left
-     * @param TransactionJournal $right
+     * @param TransactionJournal $inward
+     * @param TransactionJournal $outward
      *
      * @return mixed
      */
-    public function storeLink(array $information, TransactionJournal $left, TransactionJournal $right): TransactionJournalLink;
+    public function storeLink(array $information, TransactionJournal $inward, TransactionJournal $outward): TransactionJournalLink;
 
     /**
      * @param TransactionJournalLink $link
@@ -117,4 +145,14 @@ interface LinkTypeRepositoryInterface
      * @return LinkType
      */
     public function update(LinkType $linkType, array $data): LinkType;
+
+    /**
+     * Update an existing transaction journal link.
+     *
+     * @param TransactionJournalLink $journalLink
+     * @param array                  $data
+     *
+     * @return TransactionJournalLink
+     */
+    public function updateLink(TransactionJournalLink $journalLink, array $data): TransactionJournalLink;
 }
diff --git a/app/Transformers/JournalLinkTransformer.php b/app/Transformers/JournalLinkTransformer.php
new file mode 100644
index 0000000000..944c9f9b20
--- /dev/null
+++ b/app/Transformers/JournalLinkTransformer.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * JournalLinkTransformer.php
+ * Copyright (c) 2018 thegrumpydictator@gmail.com
+ *
+ * This file is part of Firefly III.
+ *
+ * Firefly III is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Firefly III is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace FireflyIII\Transformers;
+
+
+use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Models\Note;
+use FireflyIII\Models\TransactionJournalLink;
+use Illuminate\Support\Collection;
+use League\Fractal\Resource\Item;
+use League\Fractal\TransformerAbstract;
+use Symfony\Component\HttpFoundation\ParameterBag;
+
+/**
+ *
+ * Class JournalLinkTransformer
+ */
+class JournalLinkTransformer extends TransformerAbstract
+{
+    /**
+     * List of resources possible to include
+     *
+     * @var array
+     */
+    protected $availableIncludes = ['inward', 'outward', 'link_type'];
+    /**
+     * List of resources to automatically include
+     *
+     * @var array
+     */
+    protected $defaultIncludes = ['inward', 'outward', 'link_type'];
+
+    /** @var ParameterBag */
+    protected $parameters;
+
+    /**
+     * CurrencyTransformer constructor.
+     *
+     * @codeCoverageIgnore
+     *
+     * @param ParameterBag $parameters
+     */
+    public function __construct(ParameterBag $parameters)
+    {
+        $this->parameters = $parameters;
+    }
+
+    /**
+     * @param TransactionJournalLink $link
+     *
+     * @return Item
+     */
+    public function includeInward(TransactionJournalLink $link): Item
+    {
+        // need to use the collector to get the transaction :(
+        // journals always use collector and limited using URL parameters.
+        /** @var JournalCollectorInterface $collector */
+        $collector = app(JournalCollectorInterface::class);
+        $collector->setUser($link->source->user);
+        $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation();
+        $collector->setJournals(new Collection([$link->source]));
+        $transactions = $collector->getJournals();
+
+        return $this->item($transactions->first(), new TransactionTransformer($this->parameters), 'transactions');
+    }
+
+    /**
+     * @param TransactionJournalLink $link
+     *
+     * @return Item
+     */
+    public function includeLinkType(TransactionJournalLink $link): Item
+    {
+        return $this->item($link->linkType, new LinkTypeTransformer($this->parameters), 'link_types');
+    }
+
+    /**
+     * @param TransactionJournalLink $link
+     *
+     * @return Item
+     */
+    public function includeOutward(TransactionJournalLink $link): Item
+    {
+        // need to use the collector to get the transaction :(
+        // journals always use collector and limited using URL parameters.
+        /** @var JournalCollectorInterface $collector */
+        $collector = app(JournalCollectorInterface::class);
+        $collector->setUser($link->source->user);
+        $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation();
+        $collector->setJournals(new Collection([$link->destination]));
+        $transactions = $collector->getJournals();
+
+        return $this->item($transactions->first(), new TransactionTransformer($this->parameters), 'transactions');
+    }
+
+    /**
+     * @param TransactionJournalLink $link
+     *
+     * @return array
+     */
+    public function transform(TransactionJournalLink $link): array
+    {
+        $notes = '';
+        /** @var Note $note */
+        $note = $link->notes()->first();
+        if (null !== $note) {
+            $notes = $note->text;
+        }
+
+        $data = [
+            'id'         => (int)$link->id,
+            'updated_at' => $link->updated_at->toAtomString(),
+            'created_at' => $link->created_at->toAtomString(),
+            'notes'      => $notes,
+            'links'      => [
+                [
+                    'rel' => 'self',
+                    'uri' => '/journal_links/' . $link->id,
+                ],
+            ],
+        ];
+
+        return $data;
+    }
+}
\ No newline at end of file
diff --git a/app/Transformers/LinkTypeTransformer.php b/app/Transformers/LinkTypeTransformer.php
new file mode 100644
index 0000000000..625d390c09
--- /dev/null
+++ b/app/Transformers/LinkTypeTransformer.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * LinkTypeTransformer.php
+ * Copyright (c) 2018 thegrumpydictator@gmail.com
+ *
+ * This file is part of Firefly III.
+ *
+ * Firefly III is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Firefly III is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace FireflyIII\Transformers;
+
+
+use FireflyIII\Models\LinkType;
+use League\Fractal\TransformerAbstract;
+use Symfony\Component\HttpFoundation\ParameterBag;
+
+class LinkTypeTransformer extends TransformerAbstract
+{
+
+    /**
+     * List of resources possible to include
+     *
+     * @var array
+     */
+    protected $availableIncludes = [];
+    /**
+     * List of resources to automatically include
+     *
+     * @var array
+     */
+    protected $defaultIncludes = [];
+
+    /** @var ParameterBag */
+    protected $parameters;
+
+    /**
+     * CurrencyTransformer constructor.
+     *
+     * @codeCoverageIgnore
+     *
+     * @param ParameterBag $parameters
+     */
+    public function __construct(ParameterBag $parameters)
+    {
+        $this->parameters = $parameters;
+    }
+
+    /**
+     * Transform the currency.
+     *
+     * @param LinkType $linkType
+     *
+     * @return array
+     */
+    public function transform(LinkType $linkType): array
+    {
+        $data = [
+            'id'         => (int)$linkType->id,
+            'updated_at' => $linkType->updated_at->toAtomString(),
+            'created_at' => $linkType->created_at->toAtomString(),
+            'name'       => $linkType->name,
+            'inward'     => $linkType->inward,
+            'outward'    => $linkType->outward,
+            'editable'   => (int)$linkType->editable,
+            'links'      => [
+                [
+                    'rel' => 'self',
+                    'uri' => '/link_types/' . $linkType->id,
+                ],
+            ],
+        ];
+
+        return $data;
+    }
+}
\ No newline at end of file
diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php
index 2fae93b868..ad47b99796 100644
--- a/resources/lang/en_US/firefly.php
+++ b/resources/lang/en_US/firefly.php
@@ -1134,6 +1134,8 @@ return [
     'is (partially) refunded by_inward'     => 'is (partially) refunded by',
     'is (partially) paid for by_inward'     => 'is (partially) paid for by',
     'is (partially) reimbursed by_inward'   => 'is (partially) reimbursed by',
+    'inward_transaction'                    => 'Inward transaction',
+    'outward_transaction'                   => 'Outward transaction',
     'relates to_outward'                    => 'relates to',
     '(partially) refunds_outward'           => '(partially) refunds',
     '(partially) pays for_outward'          => '(partially) pays for',
diff --git a/resources/views/admin/link/show.twig b/resources/views/admin/link/show.twig
index 3d452df573..b9639b981c 100644
--- a/resources/views/admin/link/show.twig
+++ b/resources/views/admin/link/show.twig
@@ -15,10 +15,10 @@
                         <thead>
                         <tr>
                             <th>&nbsp;</th>
-                            <th>{{ trans('firefly.source_transaction') }}</th>
+                            <th>{{ trans('firefly.inward_transaction') }}</th>
                             <th>&nbsp;</th>
                             <th>{{ trans('firefly.link_description') }}</th>
-                            <th>{{ trans('firefly.destination_transaction') }}</th>
+                            <th>{{ trans('firefly.outward_transaction') }}</th>
                             <th>&nbsp;</th>
                         </tr>
                         </thead>
diff --git a/routes/api.php b/routes/api.php
index fb49221135..bdf9a3637e 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -143,6 +143,19 @@ Route::group(
     }
 );
 
+Route::group(
+    ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'journal_links', 'as' => 'api.v1.journal_links.'],
+    function () {
+
+        // Journal Link API routes:
+        Route::get('', ['uses' => 'JournalLinkController@index', 'as' => 'index']);
+        Route::post('', ['uses' => 'JournalLinkController@store', 'as' => 'store']);
+        Route::get('{journalLink}', ['uses' => 'JournalLinkController@show', 'as' => 'show']);
+        Route::put('{journalLink}', ['uses' => 'JournalLinkController@update', 'as' => 'update']);
+        Route::delete('{journalLink}', ['uses' => 'JournalLinkController@delete', 'as' => 'delete']);
+    }
+);
+
 Route::group(
     ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'currencies', 'as' => 'api.v1.currencies.'],
     function () {